module Eco module API module Common module People # Used with `Enumerable` of objects that have the following methods: # * `supervisor_id` # * `external_id` # * `id` module SupervisorHelpers def self.included(base) base.send(:include, ClassMethods) end module ClassMethods # Reorders as follows: # 1. supervisors, people with no supervisor or where their supervisor not present # 2. subordinates # @return [Array] `values` sorted by supervisors/subordinates def sort_by_supervisors(values, supervisors_first: true) raise "Expected non hash Enumerable. Given: #{values.class}" if values.is_a?(Hash) return [] unless values.is_a?(Enumerable) roam = proc do |tree| [].tap do |out| sub_outs = tree.empty?? [] : tree.map {|sup, subtree| roam.call(subtree)} tree.each do |sup, subtree| sout = subtree.empty?? [] : roam.call(subtree) supervisors_first ? sout.unshift(sup) : sout.push(sup) out.concat(sout) end end end roam.call(supervisors_tree(values)) end # Identifies all the cyclic supervisor chains # @note as `supervisors_tree` will have any entry involved in a cycle at the top, # it just checks all the top entries against their offspring # @return [Array] the sets of entries that are cyclic def identify_cyclic_chains(values) raise "Expected non hash Enumerable. Given: #{values.class}" if values.is_a?(Hash) return [] unless values.is_a?(Enumerable) identify = proc do |top_sup, offspring, chain = [top_sup]| next [] if offspring.empty? offspring.each_with_object([]) do |(sup, subordinates), set| break set unless set.empty? if top_sup.supervisor_id == sup.id set.concat(chain, [sup]) else set = identify.call(top_sup, subordinates, chain | [sup]) end end end supervisors_tree(values).each_with_object([]) do |(top_sup, offspring), sets| if (set = identify.call(top_sup, offspring)) && !set.empty? sets.push(set) end end end def tree_to_str(tree, lev: 0) raise "Required Hash tree structure. Given: #{tree.class}" unless tree.is_a?(Hash) "".tap do |str| tree.each do |entry, subtree| str << "#{" " * lev}+-- #{entry.id || entry.external_id}\n" str << tree_to_str(subtree, lev: lev + 1) unless !subtree || subtree.empty? end end end def print_tree(tree, lev: 0) puts tree_to_str(tree) end # Generates a `Hash` tree structure, where: # * **keys** are nodes # * **values** are `Hash` subtree structures of `key` subordinates # @note it is resilient to cyclic supervisors (it will just add the last at the top) # @param values [Enumerable] of objects with methods: # `id`, `external_id` and `supervisor_id` # @return [Hash] the tree structure def supervisors_tree(values) # rubocop:disable Metrics/AbcSize raise "Expected non hash Enumerable. Given: #{values.class}" if values.is_a?(Hash) return {} unless values.is_a?(Enumerable) idx = get_super_indexes(values) processed = [] subtree = proc do |entry, level, toptree| if processed.include?(entry) next {} unless toptree.key?(entry) && level.positive? # needs to be moved as a child subnodes = toptree.delete(entry) processed.delete(entry) end subnodes ||= {}.tap do |tree| subs = idx[:subordinates].call(entry) processed.push(entry) next nil unless subs && !subs.empty? subs.each do |sub| sub_tree = subtree.call(sub, level + 1, toptree) tree.merge!(sub_tree) end end {entry => subnodes} end {}.tap do |tree| idx[:by_sup].keys.each do |sup_id| if (sup = idx[:supers][sup_id]) tree.merge!(subtree.call(sup, 0, tree)) else idx[:by_sup][sup_id].each do |sub| tree.merge!(subtree.call(sub, 0, tree)) end end end end end private def get_super_indexes(values) # rubocop:disable Metrics/AbcSize raise "Expected non hash Enumerable. Given: #{values.class}" if values.is_a?(Hash) {}.tap do |indexes| indexes[:by_id] = values.map do |e| e.id && [e.id, e] end.compact.to_h indexes[:by_ext] = values.map do |e| e.external_id && [e.external_id, e] end.compact.to_h indexes[:by_sup] = {}.tap do |by_s| values.group_by(&:supervisor_id).tap do |by_sup| by_s[nil] = by_sup.delete(nil) if by_sup.key?(nil) by_s.merge!(by_sup) end end indexes[:supers] = {}.tap do |sups| indexes[:by_ext].select do |ext, _e| ext && indexes[:by_sup].key?(ext) end.tap {|found| sups.merge!(found)} indexes[:by_id].select do |id, _e| id && indexes[:by_sup].key?(id) end.tap {|found| sups.merge!(found)} end indexes[:is_super] = proc do |entry| is = indexes[:supers][entry.id] || indexes[:supers][entry.external_id] !is.nil? end indexes[:subordinates] = proc do |entry| subs = nil sup = indexes[:supers][entry.id] || indexes[:supers][entry.external_id] if sup subs ||= indexes[:by_sup][sup.id] if sup.id subs ||= indexes[:by_sup][sup.external_id] if sup.external_id end subs end end end end class << self include SupervisorHelpers::ClassMethods end end end end end end