sortable_events.rb 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. module SortableEvents
  2. extend ActiveSupport::Concern
  3. included do
  4. validate :validate_events_order
  5. end
  6. EVENTS_ORDER_KEY = 'events_order'.freeze
  7. EVENTS_DESCRIPTION = 'events created in each run'.freeze
  8. def description_events_order(*args)
  9. self.class.description_events_order(*args)
  10. end
  11. module ClassMethods
  12. def can_order_created_events!
  13. raise 'Cannot order events for agent that cannot create events' if cannot_create_events?
  14. prepend AutomaticSorter
  15. end
  16. def can_order_created_events?
  17. include? AutomaticSorter
  18. end
  19. def cannot_order_created_events?
  20. !can_order_created_events?
  21. end
  22. def description_events_order(events = EVENTS_DESCRIPTION, events_order_key = EVENTS_ORDER_KEY)
  23. <<~MD
  24. To specify the order of #{events}, set `#{events_order_key}` to an array of sort keys, each of which looks like either `expression` or `[expression, type, descending]`, as described as follows:
  25. * _expression_ is a Liquid template to generate a string to be used as sort key.
  26. * _type_ (optional) is one of `string` (default), `number` and `time`, which specifies how to evaluate _expression_ for comparison.
  27. * _descending_ (optional) is a boolean value to determine if comparison should be done in descending (reverse) order, which defaults to `false`.
  28. Sort keys listed earlier take precedence over ones listed later. For example, if you want to sort articles by the date and then by the author, specify `[["{{date}}", "time"], "{{author}}"]`.
  29. Sorting is done stably, so even if all events have the same set of sort key values the original order is retained. Also, a special Liquid variable `_index_` is provided, which contains the zero-based index number of each event, which means you can exactly reverse the order of events by specifying `[["{{_index_}}", "number", true]]`.
  30. #{description_include_sort_info if events == EVENTS_DESCRIPTION}
  31. MD
  32. end
  33. def description_include_sort_info
  34. <<-MD.lstrip
  35. If the `include_sort_info` option is set, each created event will have a `sort_info` key whose value is a hash containing the following keys:
  36. * `position`: 1-based index of each event after the sort
  37. * `count`: Total number of events sorted
  38. MD
  39. end
  40. end
  41. def can_order_created_events?
  42. self.class.can_order_created_events?
  43. end
  44. def cannot_order_created_events?
  45. self.class.cannot_order_created_events?
  46. end
  47. def events_order(key = EVENTS_ORDER_KEY)
  48. options[key]
  49. end
  50. def include_sort_info?
  51. boolify(interpolated['include_sort_info'])
  52. end
  53. def create_events(events)
  54. if include_sort_info?
  55. count = events.count
  56. events.each.with_index(1) do |event, position|
  57. event.payload[:sort_info] = {
  58. position:,
  59. count:
  60. }
  61. create_event(event)
  62. end
  63. else
  64. events.each do |event|
  65. create_event(event)
  66. end
  67. end
  68. end
  69. module AutomaticSorter
  70. def check
  71. return super unless events_order || include_sort_info?
  72. sorting_events do
  73. super
  74. end
  75. end
  76. def receive(incoming_events)
  77. return super unless events_order || include_sort_info?
  78. # incoming events should be processed sequentially
  79. incoming_events.each do |event|
  80. sorting_events do
  81. super([event])
  82. end
  83. end
  84. end
  85. def create_event(event)
  86. if @sortable_events
  87. event = build_event(event)
  88. @sortable_events << event
  89. event
  90. else
  91. super
  92. end
  93. end
  94. private
  95. def sorting_events(&block)
  96. @sortable_events = []
  97. yield
  98. ensure
  99. events = sort_events(@sortable_events)
  100. @sortable_events = nil
  101. create_events(events)
  102. end
  103. end
  104. private
  105. EXPRESSION_PARSER = {
  106. 'string' => ->(string) { string },
  107. 'number' => ->(string) { string.to_f },
  108. 'time' => ->(string) { Time.zone.parse(string) },
  109. }
  110. EXPRESSION_TYPES = EXPRESSION_PARSER.keys.freeze
  111. def validate_events_order(events_order_key = EVENTS_ORDER_KEY)
  112. case order_by = events_order(events_order_key)
  113. when nil
  114. when Array
  115. # Each tuple may be either [expression, type, desc] or just
  116. # expression.
  117. order_by.each do |expression, type, desc|
  118. case expression
  119. when String
  120. # ok
  121. else
  122. errors.add(:base, "first element of each #{events_order_key} tuple must be a Liquid template")
  123. break
  124. end
  125. case type
  126. when nil, *EXPRESSION_TYPES
  127. # ok
  128. else
  129. errors.add(:base,
  130. "second element of each #{events_order_key} tuple must be #{EXPRESSION_TYPES.to_sentence(last_word_connector: ' or ')}")
  131. break
  132. end
  133. if !desc.nil? && boolify(desc).nil?
  134. errors.add(:base, "third element of each #{events_order_key} tuple must be a boolean value")
  135. break
  136. end
  137. end
  138. else
  139. errors.add(:base, "#{events_order_key} must be an array of arrays")
  140. end
  141. end
  142. # Sort given events in order specified by the "events_order" option
  143. def sort_events(events, events_order_key = EVENTS_ORDER_KEY)
  144. order_by = events_order(events_order_key).presence or
  145. return events
  146. orders = order_by.map { |_, _, desc = false| boolify(desc) }
  147. Utils.sort_tuples!(
  148. events.map.with_index { |event, index|
  149. interpolate_with(event) {
  150. interpolation_context['_index_'] = index
  151. order_by.map { |expression, type, _|
  152. string = interpolate_string(expression)
  153. begin
  154. EXPRESSION_PARSER[type || 'string'.freeze][string]
  155. rescue StandardError
  156. error "Cannot parse #{string.inspect} as #{type}; treating it as string"
  157. string
  158. end
  159. }
  160. } << index << event # index is to make sorting stable
  161. },
  162. orders
  163. ).collect!(&:last)
  164. end
  165. end