sortable_events.rb 4.9KB

    module SortableEvents extend ActiveSupport::Concern included do validate :validate_events_order end EVENTS_ORDER_KEY = 'events_order'.freeze def description_events_order(*args) self.class.description_events_order(*args) end module ClassMethods def can_order_created_events! raise 'Cannot order events for agent that cannot create events' if cannot_create_events? prepend AutomaticSorter end def can_order_created_events? include? AutomaticSorter end def cannot_order_created_events? !can_order_created_events? end def description_events_order(events = 'events created in each run', events_order_key = EVENTS_ORDER_KEY) <<-MD.lstrip 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: * _expression_ is a Liquid template to generate a string to be used as sort key. * _type_ (optional) is one of `string` (default), `number` and `time`, which specifies how to evaluate _expression_ for comparison. * _descending_ (optional) is a boolean value to determine if comparison should be done in descending (reverse) order, which defaults to `false`. 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}}"]`. 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]]`. MD end end def can_order_created_events? self.class.can_order_created_events? end def cannot_order_created_events? self.class.cannot_order_created_events? end def events_order(key = EVENTS_ORDER_KEY) options[key] end module AutomaticSorter def check return super unless events_order sorting_events do super end end def receive(incoming_events) return super unless events_order # incoming events should be processed sequentially incoming_events.each do |event| sorting_events do super([event]) end end end def create_event(event) if @sortable_events event = build_event(event) @sortable_events << event event else super end end private def sorting_events(&block) @sortable_events = [] yield ensure events, @sortable_events = @sortable_events, nil sort_events(events).each do |event| create_event(event) end end end private EXPRESSION_PARSER = { 'string' => ->string { string }, 'number' => ->string { string.to_f }, 'time' => ->string { Time.zone.parse(string) }, } EXPRESSION_TYPES = EXPRESSION_PARSER.keys.freeze def validate_events_order(events_order_key = EVENTS_ORDER_KEY) case order_by = events_order(events_order_key) when nil when Array # Each tuple may be either [expression, type, desc] or just # expression. order_by.each do |expression, type, desc| case expression when String # ok else errors.add(:base, "first element of each #{events_order_key} tuple must be a Liquid template") break end case type when nil, *EXPRESSION_TYPES # ok else errors.add(:base, "second element of each #{events_order_key} tuple must be #{EXPRESSION_TYPES.to_sentence(last_word_connector: ' or ')}") break end if !desc.nil? && boolify(desc).nil? errors.add(:base, "third element of each #{events_order_key} tuple must be a boolean value") break end end else errors.add(:base, "#{events_order_key} must be an array of arrays") end end # Sort given events in order specified by the "events_order" option def sort_events(events, events_order_key = EVENTS_ORDER_KEY) order_by = events_order(events_order_key).presence or return events orders = order_by.map { |_, _, desc = false| boolify(desc) } Utils.sort_tuples!( events.map.with_index { |event, index| interpolate_with(event) { interpolation_context['_index_'] = index order_by.map { |expression, type, _| string = interpolate_string(expression) begin EXPRESSION_PARSER[type || 'string'.freeze][string] rescue error "Cannot parse #{string.inspect} as #{type}; treating it as string" string end } } << index << event # index is to make sorting stable }, orders ).collect!(&:last) end end