sortable_events.rb 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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.lstrip
  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: position,
  59. count: 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, @sortable_events = sort_events(@sortable_events), nil
  100. create_events(events)
  101. end
  102. end
  103. private
  104. EXPRESSION_PARSER = {
  105. 'string' => ->string { string },
  106. 'number' => ->string { string.to_f },
  107. 'time' => ->string { Time.zone.parse(string) },
  108. }
  109. EXPRESSION_TYPES = EXPRESSION_PARSER.keys.freeze
  110. def validate_events_order(events_order_key = EVENTS_ORDER_KEY)
  111. case order_by = events_order(events_order_key)
  112. when nil
  113. when Array
  114. # Each tuple may be either [expression, type, desc] or just
  115. # expression.
  116. order_by.each do |expression, type, desc|
  117. case expression
  118. when String
  119. # ok
  120. else
  121. errors.add(:base, "first element of each #{events_order_key} tuple must be a Liquid template")
  122. break
  123. end
  124. case type
  125. when nil, *EXPRESSION_TYPES
  126. # ok
  127. else
  128. errors.add(:base, "second element of each #{events_order_key} tuple must be #{EXPRESSION_TYPES.to_sentence(last_word_connector: ' or ')}")
  129. break
  130. end
  131. if !desc.nil? && boolify(desc).nil?
  132. errors.add(:base, "third element of each #{events_order_key} tuple must be a boolean value")
  133. break
  134. end
  135. end
  136. else
  137. errors.add(:base, "#{events_order_key} must be an array of arrays")
  138. end
  139. end
  140. # Sort given events in order specified by the "events_order" option
  141. def sort_events(events, events_order_key = EVENTS_ORDER_KEY)
  142. order_by = events_order(events_order_key).presence or
  143. return events
  144. orders = order_by.map { |_, _, desc = false| boolify(desc) }
  145. Utils.sort_tuples!(
  146. events.map.with_index { |event, index|
  147. interpolate_with(event) {
  148. interpolation_context['_index_'] = index
  149. order_by.map { |expression, type, _|
  150. string = interpolate_string(expression)
  151. begin
  152. EXPRESSION_PARSER[type || 'string'.freeze][string]
  153. rescue
  154. error "Cannot parse #{string.inspect} as #{type}; treating it as string"
  155. string
  156. end
  157. }
  158. } << index << event # index is to make sorting stable
  159. },
  160. orders
  161. ).collect!(&:last)
  162. end
  163. end