Class: Eco::API::Organization::TagTree

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/eco/api/organization/tag_tree.rb

Overview

Note:

that currenlty the parsing assumes top level to be array. This does not allow to capture the name and id of the locations structure itself into the json storing model.

Provides helpers to deal with tagtrees.

Constant Summary collapse

HEADER =
%w[
  id name weight parent_id
  archived archived_token
  classifications classification_names
  level
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(tagtree = [], name: nil, id: nil, depth: -1,, path: [], parent: nil, _weight: nil) ⇒ TagTree

Returns a new instance of TagTree.

Examples:

Node format:

{"tag": "NODE NAME", "nodes": subtree}

Tree/subtree format:

[[Node], ...]

Input format example:

tree = [{"tag" => "AUSTRALIA", "nodes" => [
    {"tag" => "SYDNEY", "nodes" => []}
]}]
tree = TagTree.new(tree.to_json)

Parameters:

  • tagtree (String) (defaults to: [])

    representation of the tagtree in json.

Raises:

  • (ArgumentError)


41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/eco/api/organization/tag_tree.rb', line 41

def initialize(
  tagtree = [],
  name:    nil,
  id:      nil,
  depth:   -1,
  path:    [],
  parent:  nil,
  _weight: nil # rubocop:disable Lint/UnderscorePrefixedVariableName
)
  @source = parse_source_input(tagtree)

  msg = "You are trying to initialize a TagTree with a null tagtree"
  raise ArgumentError, msg unless source

  @parent = parent
  @depth  = depth
  @path   = path || []

  if source.is_a?(Array)
    @id        = id
    @name      = name
    @raw_nodes = source
  else
    source['weight'] ||= _weight
    init_node
  end

  @path.push(self.id) unless top?

  @nodes = @raw_nodes.map.with_index do |cnode, idx|
    self.class.new(
      cnode,
      depth:   depth + 1,
      path:    @path.dup,
      parent:  self,
      _weight: idx
    )
  end

  init_hashes
end

Instance Attribute Details

#archivedObject

Returns the value of attribute archived.



24
25
26
# File 'lib/eco/api/organization/tag_tree.rb', line 24

def archived
  @archived
end

#archived_tokenObject

Returns the value of attribute archived_token.



24
25
26
# File 'lib/eco/api/organization/tag_tree.rb', line 24

def archived_token
  @archived_token
end

#classification_namesObject

Returns the value of attribute classification_names.



25
26
27
# File 'lib/eco/api/organization/tag_tree.rb', line 25

def classification_names
  @classification_names
end

#classificationsObject

Returns the value of attribute classifications.



25
26
27
# File 'lib/eco/api/organization/tag_tree.rb', line 25

def classifications
  @classifications
end

#depthObject (readonly)

Returns the value of attribute depth.



29
30
31
# File 'lib/eco/api/organization/tag_tree.rb', line 29

def depth
  @depth
end

#idObject Also known as: tag

Returns the value of attribute id.



20
21
22
# File 'lib/eco/api/organization/tag_tree.rb', line 20

def id
  @id
end

#nameObject

Returns the value of attribute name.



23
24
25
# File 'lib/eco/api/organization/tag_tree.rb', line 23

def name
  @name
end

#nodesObject (readonly)

Returns the value of attribute nodes.



28
29
30
# File 'lib/eco/api/organization/tag_tree.rb', line 28

def nodes
  @nodes
end

#parentObject (readonly)

Returns the value of attribute parent.



27
28
29
# File 'lib/eco/api/organization/tag_tree.rb', line 27

def parent
  @parent
end

#sourceObject (readonly)

Returns the value of attribute source.



18
19
20
# File 'lib/eco/api/organization/tag_tree.rb', line 18

def source
  @source
end

#weightObject

Returns the value of attribute weight.



23
24
25
# File 'lib/eco/api/organization/tag_tree.rb', line 23

def weight
  @weight
end

Instance Method Details

#active?Boolean

Returns:

  • (Boolean)


87
88
89
# File 'lib/eco/api/organization/tag_tree.rb', line 87

def active?
  !archived?
end

#active_treeEco::API::Organization::TagTree

Returns with non archived nodes only.

Returns:



116
117
118
# File 'lib/eco/api/organization/tag_tree.rb', line 116

def active_tree
  self.class.new(as_json(include_archived: false), name: name, id: id)
end

#all_nodes(&block) ⇒ Array<TagTree>

Note:

order is that of the parent to child relationships

All actual nodes of this tree

Returns:



161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/eco/api/organization/tag_tree.rb', line 161

def all_nodes(&block)
  [].tap do |out_nodes|
    unless top?
      out_nodes.push(self)
      yield(self) if block_given?
    end

    nodes.each do |nd|
      out_nodes.concat(nd.all_nodes(&block))
    end
  end
end

#ancestorsArray<TagTree>

Note:

it does not include the current node

All the acenstor nodes of the current node

Returns:

  • (Array<TagTree>)

    ancestors sorted from top to bottom.



177
178
179
180
181
182
183
184
# File 'lib/eco/api/organization/tag_tree.rb', line 177

def ancestors
  [].tap do |ans|
    next if parent.top?

    ans << parent
    ans.concat(parent.ancestors)
  end
end

#archived?Boolean

Returns:

  • (Boolean)


83
84
85
# File 'lib/eco/api/organization/tag_tree.rb', line 83

def archived?
  @archived
end

#as_json(include_children: true, include_archived: true, max_depth: total_depth) {|node_json, node| ... } ⇒ Array[Hash]

Returns a tree of Hashes form nested via nodes (or just a list of hash nodes)

Parameters:

  • include_children (Boolean) (defaults to: true)

    whether it should return a tree hash or just a list of hash nodes.

  • include_archived (Boolean) (defaults to: true)

    whether it should include archived nodes.

  • max_depth (Boolean) (defaults to: total_depth)

    up to what level depth nodes should be included.

Yields:

  • (node_json, node)

    block for custom output json model

Returns:

  • (Array[Hash])

    where Hash is a node (i.e. {"tag" => TAG, "nodes": Array[Hash]})



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/eco/api/organization/tag_tree.rb', line 207

def as_json( # rubocop:disable Metrics/AbcSize
  include_children: true,
  include_archived: true,
  max_depth:        total_depth,
  &block
)
  max_depth ||= total_depth
  return    if max_depth < depth
  return [] if top? && !include_children
  return    if archived? && !include_archived

  if include_children
    child_nodes   = nodes
    child_nodes   = child_nodes.select(&:active?) unless include_archived
    kargs         = {
      include_children: include_children,
      include_archived: include_archived,
      max_depth:        max_depth
    }
    children_json = child_nodes.map {|nd| nd.as_json(**kargs, &block)}.compact

    return children_json if top?
  end

  values = [
    id,       name,
    weight,   parent_id,
    archived, archived_token,
    classifications.dup,
    classification_names.dup,
    depth + 1
  ]
  node_json = self.class::HEADER.zip(values).to_h
  node_json['nodes'] = children_json if include_children
  node_json = yield(node_json, self) if block_given?
  node_json
end

#as_nodes_json(&block) ⇒ Array[Hash]

Returns a plain list form of hash nodes.

Returns:

  • (Array[Hash])

    where Hash is a plain node



247
248
249
# File 'lib/eco/api/organization/tag_tree.rb', line 247

def as_nodes_json(&block)
  all_nodes.map {|nd| nd.as_json(include_children: false, &block)}
end

#children?Boolean

Returns it has subnodes.

Returns:

  • (Boolean)

    it has subnodes



323
324
325
# File 'lib/eco/api/organization/tag_tree.rb', line 323

def children?
  children_count&.positive?
end

#children_countInteger

Returns:

  • (Integer)


318
319
320
# File 'lib/eco/api/organization/tag_tree.rb', line 318

def children_count
  nodes.count
end

#countInteger

Returns the number of locations.

Returns:

  • (Integer)

    the number of locations



257
258
259
# File 'lib/eco/api/organization/tag_tree.rb', line 257

def count
  @hash_tags.keys.count
end

#default_tag(*values) ⇒ String

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
    • to the above, take the first also on top level, but only if there's 1 level for the entire tree

Parameters:

  • values (Array<String>)

    list of tags.

Returns:

  • (String)

    default tag.



415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/eco/api/organization/tag_tree.rb', line 415

def default_tag(*values)
  default_tag    = nil
  values         = filter_tags(values)
  tnodes, ddepth = get_deepest_nodes_among_tags(*values)

  unless tnodes.empty?
    common = tnodes.reduce(tags.reverse) do |com, cnode|
      com & cnode.path.reverse
    end
    default_tag = common.first if common.any? && ddepth&.positive?
  end

  default_tag ||= tnodes.first&.tag if ddepth&.positive? || flat?
  default_tag
end

#diff(tagtree, _differences: {}, _level: 0, **options) ⇒ Array

Returns with the differences.

Returns:

  • (Array)

    with the differences



98
99
100
101
# File 'lib/eco/api/organization/tag_tree.rb', line 98

def diff(tagtree, _differences: {}, _level: 0, **options)
  require 'hashdiff'
  Hashdiff.diff(as_json, tagtree.as_json, **options.slice(:array_path, :similarity, :use_lcs))
end

#dupEco::API::Organization::TagTree

Note:

that archived nodes will also be passed over to the copy



93
94
95
# File 'lib/eco/api/organization/tag_tree.rb', line 93

def dup
  self.class.new(as_json, name: name, id: id)
end

#each {|node| ... } ⇒ Enumerable<Eco::API::Organization::TagTree>

Iterate through all the nodes of this tree

Yields:

  • (node)

    do some stuff with one of the nodes of the tree

Yield Parameters:

Returns:



129
130
131
132
133
# File 'lib/eco/api/organization/tag_tree.rb', line 129

def each(&block)
  return to_enum(:each) unless block

  all_nodes.each(&block)
end

#empty?Boolean

Returns true if there are tags in the node, false otherwise.

Returns:

  • (Boolean)

    true if there are tags in the node, false otherwise.



252
253
254
# File 'lib/eco/api/organization/tag_tree.rb', line 252

def empty?
  count <= 1
end

#filter_tags(list) ⇒ Array<String>

Filters tags out that do not belong to the tree

Parameters:

  • list (Array<String>)

    source tags.

Returns:

  • (Array<String>)


347
348
349
350
351
# File 'lib/eco/api/organization/tag_tree.rb', line 347

def filter_tags(list)
  return [] unless list.is_a?(Array)

  list.select {|str| tag?(str)}
end

#flat?Integer

Returns if there's only top level.

Returns:

  • (Integer)

    if there's only top level.



272
273
274
# File 'lib/eco/api/organization/tag_tree.rb', line 272

def flat?
  total_depth <= 0
end

#leafsArray<String>

Returns all the tags with no children

Returns:

  • (Array<String>)


311
312
313
314
315
# File 'lib/eco/api/organization/tag_tree.rb', line 311

def leafs
  tags.reject do |tag|
    node(tag).children?
  end
end

#merge(other) ⇒ Eco::API::Organization::TagTree

Note:

it merges the first level nodes (and their children) as it comes

It generates a merged tagtree out of two sources

Returns:

Raises:

  • (ArgumentError)


106
107
108
109
110
111
112
113
# File 'lib/eco/api/organization/tag_tree.rb', line 106

def merge(other)
  msg = "Expecting Eco::API::Organization::TagTree. Given: #{other.class}"
  raise ArgumentError, msg unless other.is_a?(Eco::API::Organization::TagTree)

  mid   = [id, other.id].join('|')
  mname = [name, other.name].join('|')
  self.class.new(as_json | other.as_json, id: mid, name: mname)
end

#node(key) ⇒ TagTree?

Finds a subtree node.

Parameters:

  • key (String)

    parent node of subtree.

Returns:

  • (TagTree, nil)

    if the tag key is a node, returns that node.



338
339
340
341
342
# File 'lib/eco/api/organization/tag_tree.rb', line 338

def node(key)
  return nil unless tag?(key)

  @hash_tags[key.upcase]
end

#parent_idString

Returns the id of the parent (unless we are on a top level node).

Returns:

  • (String)

    the id of the parent (unless we are on a top level node)



187
188
189
# File 'lib/eco/api/organization/tag_tree.rb', line 187

def parent_id
  parent.id unless parent.top?
end

#parent_nameString

Returns the name of the parent (unless we are on a top level node).

Returns:

  • (String)

    the name of the parent (unless we are on a top level node)



192
193
194
# File 'lib/eco/api/organization/tag_tree.rb', line 192

def parent_name
  parent.name unless parent.top?
end

#path(key = nil) ⇒ Array<String>

Note:

the path is not relative to the subtree, but absolute to the entire tree.

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.

Parameters:

  • key (String) (defaults to: nil)

    tag to find the path to.

Returns:

  • (Array<String>)


358
359
360
361
362
# File 'lib/eco/api/organization/tag_tree.rb', line 358

def path(key = nil)
  return @path.dup unless key

  @hash_paths[key.upcase].dup
end

#reject(&block) ⇒ Array<TagTree>

Note:

rejected nodes will not include their children nodes

Returns plain list of nodes.

Returns:

  • (Array<TagTree>)

    plain list of nodes



154
155
156
# File 'lib/eco/api/organization/tag_tree.rb', line 154

def reject(&block)
  select(when_is: false, &block)
end

#select(when_is: true, &block) ⇒ Array<TagTree>

Note:

rejected nodes will not include their children nodes

Returns plain list of nodes.

Returns:

  • (Array<TagTree>)

    plain list of nodes

Raises:

  • (ArgumentError)


137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/eco/api/organization/tag_tree.rb', line 137

def select(when_is: true, &block)
  raise ArgumentError, "Missing block" unless block_given?
  [].tap do |out_nodes|
    selected = false

    selected = (yield(self) == when_is) unless top?
    out_nodes.push(self)                if selected
    next                                unless selected || top?

    nodes.each do |nd|
      out_nodes.concat(nd.select(when_is: when_is, &block))
    end
  end
end

#subtag?(key) ⇒ Boolean Also known as: subid?

Verifies if a tag exists in the subtree(s).

Parameters:

  • key (String)

    tag to verify.

Returns:

  • (Boolean)


304
305
306
# File 'lib/eco/api/organization/tag_tree.rb', line 304

def subtag?(key)
  subtags.include?(key&.upcase)
end

#subtagsArray<String> Also known as: subids

Gets all but the upper level tags of the current node tree.

Returns:

  • (Array<String>)


296
297
298
# File 'lib/eco/api/organization/tag_tree.rb', line 296

def subtags
  tags - tags(depth: depth)
end

#tag?(key) ⇒ Boolean Also known as: id?

Verifies if a tag exists in the tree.

Parameters:

  • key (String)

    tag to verify.

Returns:

  • (Boolean)


330
331
332
# File 'lib/eco/api/organization/tag_tree.rb', line 330

def tag?(key)
  @hash_tags.key?(key&.upcase)
end

#tags(depth: nil) ⇒ Array<String> Also known as: ids

Note:
  • this will include the upper level tag(s) as well
  • to get all but the upper level tag(s) use subtags method instead

Gets all the tags of the current node tree.

Parameters:

  • depth (Integer) (defaults to: nil)

    if empty, returns the list of tag nodes of that level. Otherwise the list of tag nodes of the entire subtree.

Returns:

  • (Array<String>)


283
284
285
286
287
288
289
290
291
# File 'lib/eco/api/organization/tag_tree.rb', line 283

def tags(depth: nil)
  if !depth || depth&.negative?
    @hash_tags.keys
  else
    @hash_tags.select do |_t, n|
      n.depth == depth
    end.keys
  end
end

#top?Boolean

Returns:

  • (Boolean)


196
197
198
# File 'lib/eco/api/organization/tag_tree.rb', line 196

def top?
  depth == -1
end

#total_depthInteger

Returns the highest depth of all the children.

Returns:

  • (Integer)

    the highest depth of all the children.



262
263
264
265
266
267
268
269
# File 'lib/eco/api/organization/tag_tree.rb', line 262

def total_depth
  @total_depth ||=
    if children?
      nodes.max_by(&:total_depth)&.total_depth
    else
      depth
    end
end

#truncate(max_depth: total_depth) ⇒ Eco::API::Organization::TagTree

Returns with nodes up to max_depth.

Returns:



121
122
123
# File 'lib/eco/api/organization/tag_tree.rb', line 121

def truncate(max_depth: total_depth)
  self.class.new(as_json(max_depth: max_depth), name: name, id: id)
end

#user_tags(initial: [], final: [], preserve_custom: true, add_custom: false) ⇒ Array<String>

Helper to assign tags to a person account.

  • It preserves the :initial order, in case the :final tags are the same

Examples:

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"]

Parameters:

  • initial (Array<String>) (defaults to: [])

    original tags a person has in their account.

  • final (Array<String>) (defaults to: [])

    target tags the person should have in their account afterwards.

  • preserve_custom (Boolean) (defaults to: true)

    indicates if original tags that are not in the tree should be added/preserved.

  • add_custom (Boolean) (defaults to: false)

    indicates if target tags that are not in the tree should be really added.

Returns:

  • (Array<String>)

    with the treated final tags.



394
395
396
397
398
399
400
401
402
403
404
405
# File 'lib/eco/api/organization/tag_tree.rb', line 394

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    |= custom if preserve_custom
  new_tags  = final - initial
  # keep same order as they where
  (initial & final) + new_tags
end