module Eco module API module Organization # Provides helpers to deal with tagtrees. class TagTree attr_reader :tag, :nodes attr_reader :depth, :path attr_reader :enviro # @example Node format: # {"tag": "NODE NAME", "nodes": subtree} # @example Tree/subtree format: # [[Node], ...] # @example Input format example: # tree = [{"tag" => "AUSTRALIA", "nodes" => [ # {"tag" => "SYDNEY", "nodes" => []} # ]}] # tree = TagTree.new(tree.to_json) # @param tagtree [String] representation of the tagtree in json. def initialize(tagtree = [], depth: -1, path: [], enviro: nil) case tagtree when String @source = JSON.parse(tagtree) else @source = tagtree end fatal("You are trying to initialize a TagTree with a null tagtree") if !@source fatal("Expecting Environment object. Given: #{enviro}") if enviro && !enviro.is_a?(API::Common::Session::Environment) @enviro = enviro @depth = depth @tag = @source.is_a?(Array) ? nil : @source.dig('tag')&.upcase @path = path || [] @path.push(@tag) unless !@tag nodes = @source.is_a?(Array) ? @source : @source.dig('nodes') || [] @nodes = nodes.map {|cnode| TagTree.new(cnode, depth: @depth + 1, path: @path.dup, enviro: @enviro)} init_hashes end # @return [Boolean] `true` if there are tags in the node, `false` otherwise. def empty? @has_tags.empty? end # @param depth [Integer] if empty, returns the list of tag nodes of that level. Otherwise the list of tag nodes of the entire subtree. # @return [Array] def tags(depth: nil) if !depth || depth < 0 @hash_tags.keys else @hash_tags.select do |t, n| n.depth == depth end.keys end end # Verifies if a tag exists in the tree. # @param key [String] tag to verify. # @return [Boolean] def tag?(key) @hash_tags.key?(key&.upcase) end # Finds a subtree node. # @param key [String] parent node of subtree. # @return [TagTree, nil] if the tag `key` is a node, returns that node. def node(key) return nil unless tag?(key) @hash_tags[key.upcase] end # Filters tags out that do not belong to the tree # @param list [Array] source tags. # @return [Array] def filter_tags(list) return [] unless list && list.is_a?(Array) list.select {|str| tag?(str)} end # Finds the path from a node `key` to its root node in the tree. # If `key` is not specified, returns the path from current node to root. # @note the `path` is not relative to the subtree, but absolute to the entire tree. # @param key [String] tag to find the path to. # @return [Array] def path(key = nil) return @path if !key @hash_paths[key.upcase] end # Helper to assign tags to a person account. # * It preserves the `:initial` order, in case the `:final` tags are the same # # @example Usage example: # tree = [{"tag" => "Australia", "nodes" => [ # {"tag" => "SYDNEY", "nodes" => []}, # {"tag" => "MELBOURNE", "nodes" => []} # ]}] # # tree = TagTree.new(tree.to_json) # original = ["SYDNEY", "RISK"] # final = ["MELBOURNE", "EVENT"] # # tree.user_tags(initial: original, final: final) # out: ["MELBOURNE", "RISK"] # tree.user_tags(initial: original, final: final, preserve_custom: false) # out: ["MELBOURNE"] # tree.user_tags(initial: original, final: final, add_custom: true) # out: ["MELBOURNE", "RISK", "EVENT"] # tree.user_tags(initial: original, final: final, preserve_custom: false, add_custom: true) # out: ["MELBOURNE", "EVENT"] # # @param initial [Array] original tags a person has in their account. # @param final [Array] target tags the person should have in their account afterwards. # @param preserve_custom [Boolean] indicates if original tags that are not in the tree should be added/preserved. # @param add_custom [Boolean] indicates if target tags that are not in the tree should be really added. # @return [Array] with the treated final tags. def user_tags(initial: [], final: [], preserve_custom: true, add_custom: false) initial = [initial].flatten.compact final = [final].flatten.compact raise "Expected Array for initial: and final:" unless initial.is_a?(Array) && final.is_a?(Array) final = filter_tags(final) unless add_custom custom = initial - filter_tags(initial) final = final + custom if preserve_custom new_tags = final - initial # keep same order as they where (initial & final) + new_tags end # Helper to decide which among the tags will be the default. # * take the deepest tag (the one that is further down in the tree) # * if there are different options (several nodes at the same depth): # * take the common node between them (i.e. you have Hamilton and Auckland -> take New Zealand) # * if there's no common node between them, take the `first` (unless they are at top level of the tree) # @param [Array] values list of tags. # @return [String] default tag. def default_tag(*values) values = filter_tags(values) nodes = []; depth = -1 values.each do |tag| raise("Couldn't find the node of #{tag} in the tag-tree definition") unless cnode = node(tag) if cnode.depth > depth nodes = [cnode] depth = cnode.depth elsif cnode.depth == depth nodes.push(cnode) end end default_tag = nil if nodes.length > 1 common = nodes.reduce(self.tags.reverse) {|com, cnode| com & cnode.path.reverse} default_tag = common.first if common.length > 0 && depth > 0 end default_tag = nodes.first&.tag if !default_tag && depth > 0 default_tag end protected def hash @hash_tags end def hash_paths @hash_paths end private def init_hashes @hash_tags = {} @hash_tags[@tag] = self unless !@tag @hash_tags = @nodes.reduce(@hash_tags) do |h,n| h.merge(n.hash) end @hash_paths = {} @hash_paths[@tag] = @path @hash_paths = @nodes.reduce(@hash_paths) do |h,n| h.merge(n.hash_paths) end end def fatal(msg) raise msg if !@enviro @enviro.logger.fatal(msg) raise msg end def warn(msg) raise msg if !@enviro @enviro.logger.warn(msg) end end end end end