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 def sort_by_supervisors(values, supervisors_first: true) raise "Expected non hash Enumerable. Given: #{values.class}" if values.is_a?(Hash) return [] unless values && values.is_a?(Enumerable) roam = Proc.new 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 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) raise "Expected non hash Enumerable. Given: #{values.class}" if values.is_a?(Hash) return {} unless values && values.is_a?(Enumerable) idx = get_super_indexes(values) processed = [] subtree = Proc.new do |entry, level, toptree| if processed.include?(entry) next {} unless toptree.key?(entry) && level > 0 # 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) 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 do |e| e.supervisor_id end.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.new do |entry| !!(indexes[:supers][entry.id] || indexes[:supers][entry.external_id]) end indexes[:subordinates] = Proc.new do |entry| subs = nil if sup = indexes[:supers][entry.id] || indexes[:supers][entry.external_id] subs ||= indexes[:by_sup][sup.id] unless !sup.id subs ||= indexes[:by_sup][sup.external_id] unless !sup.external_id end subs end end end end class << self include SupervisorHelpers::ClassMethods end end end end end end