eco-helpers 2.4.3 → 2.4.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84e0b3626647d968a77ecc7dbc9dbe052be392ff7590f9f32c2402d4a0fdb45b
4
- data.tar.gz: cfd6e253e91d50bd62ffb4cbad8556214a3519ccaf9fdd148c681170ad150466
3
+ metadata.gz: 333d58d63c6ffad6b06865c89dce3e48a50347ba998361f34eb9a2e230ea83f2
4
+ data.tar.gz: 7be152e20c00765217b6650790dfd4aeba729bb614e6d0928a7e7ea82d94de33
5
5
  SHA512:
6
- metadata.gz: 4162b785935a8f161ad5030e8ffbd72defd3ed10f50b6d4944b219c0faed84f5cd57efee5a1c43ed41c2f4efdbbcb5ab7356c426dbecd2ebaca2a0da79bba90a
7
- data.tar.gz: 5c5ad42b25f53b92fcac214555fb7b2ab9b5af0c033367b375b6ae55334ce0ffa7ecca74d05bac7dd3a75aba7761d9f7be91d5fab44310eb664eeed3df758f83
6
+ metadata.gz: 3cb79f7d5429bf4c3cd8b8b37cbbdb2b8208da2161559fb6a09b08a67cea1ded2d08355928af4ee136b39bc10fda3f82ece275f5991c65bfcf41c4290d7d7269
7
+ data.tar.gz: 78723001238f02cac999e2af5200e1e13f444572366291357a70ec951732ded65bd708be9b6c0259a9e6bf797fc5c803df17a28167d01b1bf1223900f8226cc7
data/CHANGELOG.md CHANGED
@@ -1,12 +1,42 @@
1
1
  # Change Log
2
2
  All notable changes to this project will be documented in this file.
3
3
 
4
- ## [2.4.4] - 2023-03-xx
4
+ ## [2.4.6] - 2023-03-xx
5
5
 
6
6
  ### Added
7
7
  ### Changed
8
8
  ### Fixed
9
9
 
10
+ ## [2.4.5] - 2023-03-31
11
+
12
+ ### Added
13
+ - `Eco::API:Organization::TagTree` support for `archived` and `weight` properties
14
+ - `Eco::Data::Hashes::ArrayDiff` and `Eco::Data::Hashes::DiffResult`
15
+ - Enable easy comparison of array of hashes
16
+ - Input data can be diverse
17
+
18
+ ### Fixed
19
+ - `Eco::API::UseCases::DefaultCases::CsvToTree`
20
+ - Fixed the missed alignment children that jump levels
21
+ - `Eco::API::UseCases::DefaultCases::CsvToTree::Node`
22
+ - `to_h`: attrs param was being ingnored.
23
+
24
+
25
+ ## [2.4.4] - 2023-03-29
26
+
27
+ ### Added
28
+ - `Eco::API:Organization::TagTree`
29
+ - Added **methods** `#each` and `#all_nodes` allow to loop through all nodes
30
+ - Added `parent` to refer to the parent node.
31
+
32
+ ### Changed
33
+ - `Eco::API:Organization::TagTree` made **Enumerable**
34
+
35
+ ### Fixed
36
+ - `Eco:API::Session#live_tree` pass `enviro`
37
+ - `Eco::API::UseCases::OozeSamples::RegisterUpdateCase`
38
+ - Fix typo on error message.
39
+
10
40
  ## [2.4.3] - 2023-03-23
11
41
 
12
42
  ### Fixed
@@ -9,6 +9,7 @@ module Eco
9
9
  # Helper factory class to generate entries (input entries).
10
10
  # @attr_reader schema [Ecoportal::API::V1::PersonSchema] person schema to be used in this entry factory
11
11
  class EntryFactory < Eco::API::Common::Session::BaseSession
12
+ include Eco::Data::Files
12
13
 
13
14
  attr_reader :schema, :person_parser
14
15
 
@@ -111,7 +112,10 @@ module Eco
111
112
  end
112
113
  # Get content only when it's not :xls
113
114
  # note: even if content was provided, file takes precedence
114
- content = get_file_content(file, format, encoding) if (format != :xls) && file
115
+ if (format != :xls) && file
116
+ content = get_file_content(file, encoding)
117
+ end
118
+ #content = get_file_content(file, format, encoding) if (format != :xls) && file
115
119
 
116
120
  case content
117
121
  when Hash
@@ -142,10 +146,8 @@ module Eco
142
146
  entry_hash["source_file"] = file
143
147
  end
144
148
  end
145
-
146
149
  end
147
150
 
148
-
149
151
  # Helper that generates a file out of `data:`.
150
152
  # @raise Exception
151
153
  # - if you try to provide `data:` in the wrong format.
@@ -164,7 +166,7 @@ module Eco
164
166
  fatal("There is no parser/serializer for format ':#{format.to_s}'") unless @person_parser.defined?(format)
165
167
 
166
168
  run = true
167
- if Eco::API::Common::Session::FileManager.file_exists?(file)
169
+ if self.class.file_exists?(file)
168
170
  prompt_user("Do you want to overwrite it? (Y/n):", explanation: "The file '#{file}' already exists.", default: "Y") do |response|
169
171
  run = (response == "") || response.upcase.start_with?("Y")
170
172
  end
@@ -183,41 +185,10 @@ module Eco
183
185
  fd.write(person_parser.serialize(format, data_entries))
184
186
  end
185
187
  end
186
-
187
188
  end
188
189
 
189
190
  private
190
191
 
191
- def get_file_content(file, format, encoding)
192
- unless Eco::API::Common::Session::FileManager.file_exists?(file)
193
- logger.error("File does not exist: #{file}")
194
- exit(1)
195
- end
196
- #ext = File.extname(file)
197
- encoding ||= Eco::API::Common::Session::FileManager.encoding(file)
198
- encoding = (encoding == "bom") ? "#{encoding}|utf-8": encoding
199
- puts "File encoding: '#{encoding}'" unless !encoding || encoding == 'utf-8'
200
- read_with_tolerance(file, encoding: encoding)
201
- end
202
-
203
- def read_with_tolerance(file, encoding:)
204
- if content = File.read(file, encoding: encoding)
205
- content = content.encode("utf-8") unless encoding.include?('utf-8')
206
- tolerance = 5
207
- content.scrub do |bytes|
208
- replacement = '<' + bytes.unpack('H*')[0] + '>'
209
- if tolerance <= 0
210
- logger.error("There were more than 5 encoding errors in the file '#{file}'.")
211
- return content
212
- else
213
- tolerance -= 1
214
- logger.error("Encoding problem in file '#{file}': '#{replacement}'.")
215
- replacement
216
- end
217
- end
218
- end
219
- end
220
-
221
192
  def fatal(msg)
222
193
  logger.fatal(msg)
223
194
  raise msg
@@ -88,7 +88,6 @@ module Eco
88
88
  File.open(file, mode) { |fd| fd << content + "\n" } # '\n' won't add line
89
89
  return file
90
90
  end
91
-
92
91
  end
93
92
  end
94
93
  end
@@ -5,12 +5,15 @@ module Eco
5
5
  class TagTree
6
6
  attr_accessor :id
7
7
  alias_method :tag, :id
8
- attr_accessor :name
8
+ attr_accessor :name, :archived, :weight
9
9
 
10
+ attr_reader :parent
10
11
  attr_reader :nodes, :children_count
11
12
  attr_reader :depth, :path
12
13
  attr_reader :enviro
13
14
 
15
+ include Enumerable
16
+
14
17
  # @example Node format:
15
18
  # {"tag": "NODE NAME", "nodes": subtree}
16
19
  # @example Tree/subtree format:
@@ -21,8 +24,9 @@ module Eco
21
24
  # ]}]
22
25
  # tree = TagTree.new(tree.to_json)
23
26
  # @param tagtree [String] representation of the tagtree in json.
24
- def initialize(tagtree = [], name: nil, id: nil, depth: -1, path: [], enviro: nil)
25
- @depth = depth
27
+ def initialize(tagtree = [], name: nil, id: nil, depth: -1, path: [], parent: nil, _weight: nil, enviro: nil)
28
+ @depth = depth
29
+ @parent = parent
26
30
 
27
31
  case tagtree
28
32
  when String
@@ -35,30 +39,57 @@ module Eco
35
39
  @enviro = enviro
36
40
 
37
41
  if @source.is_a?(Array)
38
- @id = id
39
- @name = name
42
+ @id = id
43
+ @name = name
40
44
  @row_nodes = @source
41
45
  else
42
- @id = @source.values_at('tag', 'id').compact.first&.upcase
43
- @name = @source['name']
46
+ @id = @source.values_at('tag', 'id').compact.first&.upcase
47
+ @name = @source['name']
48
+ @archived = @source.fetch('archived', false)
49
+ @weight = @source.fetch('weight', _weight)
44
50
  @row_nodes = @source['nodes'] || []
45
51
  end
46
52
 
47
53
  @path = path || []
48
54
  @path.push(@id) unless top?
49
55
 
50
- @nodes = @row_nodes.map do |cnode|
51
- TagTree.new(cnode, depth: depth + 1, path: @path.dup, enviro: @enviro)
56
+ @nodes = @row_nodes.map.with_index do |cnode, idx|
57
+ TagTree.new(cnode, depth: depth + 1, path: @path.dup, parent: self, _weight: idx, enviro: @enviro)
52
58
  end
53
59
 
54
60
  init_hashes
55
61
  end
56
62
 
63
+ def archived?
64
+ @archived
65
+ end
66
+
57
67
  # @return [Eco::API::Organization::TagTree]
58
68
  def dup
59
69
  self.class.new(as_json)
60
70
  end
61
71
 
72
+ # Iterate through all the nodes of this tree
73
+ # @yield [node] do some stuff with one of the nodes of the tree
74
+ # @yieldparam node [Eco::API::Organization::TagTree] a node of the tree
75
+ # @return [Enumerable<Eco::API::Organization::TagTree>]
76
+ def each(&block)
77
+ return to_enum(:each) unless block
78
+ all_nodes.each(&block)
79
+ end
80
+
81
+ # All actual nodes of this tree
82
+ # @note order is that of the parent to child relationships
83
+ # @return [Array[]]
84
+ def all_nodes(&block)
85
+ [].tap do |all_nodes|
86
+ all_nodes.push(self) unless top?
87
+ nodes.each do |node|
88
+ all_nodes.concat(node.all_nodes)
89
+ end
90
+ end
91
+ end
92
+
62
93
  # @return [Array] with the differences
63
94
  def diff(tagtree, differences: {}, level: 0, **options)
64
95
  require 'hashdiff'
@@ -20,9 +20,9 @@ module Eco
20
20
  end
21
21
 
22
22
  # Among all the locations structures it selects the one with more location nodes
23
- def live_tree(enviro: nil)
23
+ def live_tree(enviro: nil, include_archived: false)
24
24
  return @live_tree if instance_variable_defined?(:@live_tree) && @live_tree.enviro == enviro
25
- trees = live_trees(enviro: enviro)
25
+ trees = live_trees(enviro: enviro, include_archived: include_archived)
26
26
  @live_tree = trees.reject do |tree|
27
27
  tree.empty?
28
28
  end.max do |a,b|
@@ -36,12 +36,12 @@ module Eco
36
36
  end
37
37
 
38
38
  # Retrieves all the location structures of the organisation
39
- def live_trees(enviro: nil)
39
+ def live_trees(enviro: nil, include_archived: false)
40
40
  [].tap do |eco_trees|
41
41
  next unless apis.active_api.version_available?(:graphql)
42
42
  next unless graphql = apis.api(version: :graphql)
43
43
  kargs = {
44
- includeArchived: false,
44
+ includeArchived: include_archived,
45
45
  includeUnpublished: false
46
46
  }
47
47
  next unless trees = graphql.currentOrganization.locationStructures(**kargs)
@@ -45,8 +45,8 @@ module Eco
45
45
  end
46
46
 
47
47
  # @see Eco::API::Session::Config#live_tree
48
- def live_tree
49
- config.live_tree
48
+ def live_tree(include_archived: false)
49
+ config.live_tree(enviro: enviro)
50
50
  end
51
51
 
52
52
  # @see Eco::API::Session::Config#schemas
@@ -14,9 +14,9 @@ class Eco::API::UseCases::DefaultCases::CsvToTree
14
14
  result
15
15
  end
16
16
 
17
- def csv_nodes(filename)
17
+ def nodes_from_csv(csv)
18
18
  i = 1; prev_level = nil; prev_node = nil; prev_nodes = Array(1..11).zip(Array.new(11, nil)).to_h
19
- nodes = csv_from(filename).each_with_object([]) do |row, out|
19
+ nodes = csv.each_with_object([]) do |row, out|
20
20
  values = row.fields.map do |value|
21
21
  value = value.to_s.strip
22
22
  value.empty?? nil : value
@@ -41,6 +41,10 @@ class Eco::API::UseCases::DefaultCases::CsvToTree
41
41
  tidy_nodes(nodes)
42
42
  end
43
43
 
44
+ def csv_nodes(filename)
45
+ nodes_from_csv(csv_from(filename))
46
+ end
47
+
44
48
  private
45
49
 
46
50
  def csv_from_content(filename)
@@ -25,19 +25,29 @@ class Eco::API::UseCases::DefaultCases::CsvToTree
25
25
 
26
26
  def tag
27
27
  raw_tag.yield_self do |str|
28
- partial = replace_not_allowed(str)
28
+ blanks_x2 = has_double_blanks?(str)
29
+ partial = replace_not_allowed(str)
29
30
  remove_double_blanks(partial).tap do |result|
31
+ next if invalid_warned?
30
32
  if partial != str
31
33
  invalid_chars = identify_invalid_characters(str)
32
34
  puts "• (Row: #{self.row_num}) Invalid characters _#{invalid_chars}_ (removed): '#{str}' (converted to '#{result}')"
35
+ elsif blanks_x2
36
+ puts "• (Row: #{self.row_num}) Double blanks (removed): '#{str}' (converted to '#{result}')"
33
37
  end
34
- if result != partial
35
- #puts "• There were DOUBLE BLANKS: tag '#{str}' (converted to '#{result}')"
36
- end
38
+ invalid_warned!
37
39
  end
38
40
  end
39
41
  end
40
42
 
43
+ def invalid_warned?
44
+ @invalid_warned ||= false
45
+ end
46
+
47
+ def invalid_warned!
48
+ @invalid_warned = true
49
+ end
50
+
41
51
  def raw_tag
42
52
  values_at(*TAGS_ATTRS.reverse).compact.first
43
53
  end
@@ -163,7 +173,7 @@ class Eco::API::UseCases::DefaultCases::CsvToTree
163
173
 
164
174
  def to_h(*attrs)
165
175
  attrs = ALL_ATTRS if attrs.empty?
166
- ALL_ATTRS.zip(values_at(*ALL_ATTRS)).to_h
176
+ attrs.zip(values_at(*attrs)).to_h
167
177
  end
168
178
 
169
179
  def slice(*attrs)
@@ -188,6 +198,11 @@ class Eco::API::UseCases::DefaultCases::CsvToTree
188
198
  TAGS_ATTRS.length
189
199
  end
190
200
 
201
+ def has_double_blanks?(str)
202
+ return false if str.nil?
203
+ str.match(DOUBLE_BLANKS)
204
+ end
205
+
191
206
  def remove_double_blanks(str)
192
207
  return nil if str.nil?
193
208
  str.gsub(DOUBLE_BLANKS, ' ').strip
@@ -12,7 +12,7 @@ class Eco::API::UseCases::DefaultCases::CsvToTree
12
12
  nodes.tap do |nodes|
13
13
  prev_nodes = Array(1..11).zip(Array.new(11, nil)).to_h
14
14
  nodes.each do |node|
15
- if parent_node = prev_nodes[node.raw_level - 1]
15
+ if parent_node = prev_nodes[node.actual_level - 1]
16
16
  node.parentId = parent_node.id
17
17
  end
18
18
  prev_nodes[node.raw_level] = node
@@ -15,10 +15,16 @@ class Eco::API::UseCases::GraphQL::Base < Eco::API::Common::Loaders::UseCase
15
15
  raise "You need to inherit from this class ('#{self.class}') and call super with a block"
16
16
  end
17
17
 
18
+ private
19
+
18
20
  def graphql
19
21
  @graphql ||= session.api(version: :graphql)
20
22
  end
21
23
 
24
+ def simulate?
25
+ options.dig(:simulate)
26
+ end
27
+
22
28
  def exit_error(msg)
23
29
  logger.error(msg)
24
30
  exit(1)
@@ -0,0 +1,4 @@
1
+ module Eco::API::UseCases::GraphQL::Helpers::Locations
2
+ class Commands
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Eco::API::UseCases::GraphQL::Helpers
2
+ module Locations
3
+ end
4
+ end
5
+
6
+ require_relative 'locations/commands'
@@ -0,0 +1,6 @@
1
+ module Eco::API::UseCases::GraphQL
2
+ module Helpers
3
+ end
4
+ end
5
+
6
+ require_relative 'helpers/locations'
@@ -8,3 +8,4 @@ module Eco
8
8
  end
9
9
 
10
10
  require_relative 'graphql/base'
11
+ require_relative 'graphql/helpers'
@@ -109,7 +109,7 @@ class Eco::API::UseCases::OozeSamples::RegisterUpdateCase < Eco::API::UseCases::
109
109
  if dirty?(pending)
110
110
  msg = "Inconsistent search results. "
111
111
  msg << "Launching update on '#{object_reference(pending)}' to be able to queue it back"
112
- console.warn msg
112
+ logger.warn msg
113
113
  update_ooze(pending)
114
114
  end
115
115
  end
@@ -118,7 +118,7 @@ class Eco::API::UseCases::OozeSamples::RegisterUpdateCase < Eco::API::UseCases::
118
118
  yield(ooz)
119
119
  else
120
120
  @non_retrieved_oozes += 1
121
- console.warn "Could not get page #{page_result.id}"
121
+ logger.warn "Could not get page #{page_result.id}"
122
122
  end
123
123
  end
124
124
  update_oozes
@@ -9,9 +9,44 @@ module Eco
9
9
  base.extend(ClassMethods)
10
10
  end
11
11
  end
12
-
13
- module InstanceMethods
14
12
 
13
+ module InstanceMethods
14
+ include Eco::Language::AuxiliarLogger
15
+
16
+ # It offers a resilient way to read content from a file
17
+ # @tolerance [Integer] the number of allowed encoding errors.
18
+ # @return [String] the content of the file
19
+ def get_file_content(file, encoding, tolerance: 5)
20
+ unless self.class.file_exists?(file)
21
+ logger.error("File does not exist: #{file}")
22
+ exit(1)
23
+ end
24
+ encoding ||= self.class.encoding(file)
25
+ encoding = (encoding == "bom") ? "#{encoding}|utf-8": encoding
26
+ unless !encoding || encoding == 'utf-8'
27
+ msg = "File encoding: '#{encoding}'"
28
+ logger.debug(msg)
29
+ puts msg
30
+ end
31
+ read_with_tolerance(file, encoding: encoding, tolerance: tolerance)
32
+ end
33
+
34
+ def read_with_tolerance(file, encoding:, tolerance: 5)
35
+ if content = File.read(file, encoding: encoding)
36
+ content = content.encode("utf-8") unless encoding.include?('utf-8')
37
+ content.scrub do |bytes|
38
+ replacement = '<' + bytes.unpack('H*')[0] + '>'
39
+ if tolerance <= 0
40
+ logger.error("There were more than 5 encoding errors in the file '#{file}'.")
41
+ return content
42
+ else
43
+ tolerance -= 1
44
+ logger.error("Encoding problem in file '#{file}': '#{replacement}'.")
45
+ replacement
46
+ end
47
+ end
48
+ end
49
+ end
15
50
  end
16
51
 
17
52
  module ClassMethods
@@ -86,12 +121,28 @@ module Eco
86
121
  path = File.dirname($0)
87
122
  File.join(path, basename)
88
123
  end
124
+
125
+ def folder_files(folder = ".", pattern = "*", regexp: nil, older_than: nil)
126
+ target = File.join(File.expand_path(folder), pattern)
127
+ Dir[target].tap do |dir_files|
128
+ dir_files.select! {|f| File.file?(f)}
129
+ if older_than
130
+ dir_files.select! {|f| File.mtime(f) < (Time.now - (60*60*24*older_than))}
131
+ end
132
+ if regexp && regexp.is_a?(Regexp)
133
+ dir_files.select! {|f| File.basename(f).match(regexp)}
134
+ end
135
+ end.sort
136
+ end
137
+
138
+ def csv_files(folder = ".", regexp: nil, older_than: nil)
139
+ folder_files(folder, "*.csv", regexp: regexp, older_than: older_than).sort
140
+ end
89
141
  end
90
142
 
91
143
  class << self
92
144
  include Files::ClassMethods
93
145
  end
94
-
95
146
  end
96
147
  end
97
148
  end
@@ -0,0 +1,154 @@
1
+ module Eco
2
+ module Data
3
+ module Hashes
4
+ class ArrayDiff
5
+ extend Eco::Language::Models::ClassHelpers
6
+
7
+ class << self
8
+ def key(value = nil)
9
+ return @key unless value
10
+ @key = value.to_s
11
+ end
12
+
13
+ def key?
14
+ !!@key
15
+ end
16
+
17
+ def compare(*attrs)
18
+ compared_attrs.push(*attrs.map(&:to_s)).uniq!
19
+ end
20
+
21
+ def case_sensitive(value = nil)
22
+ @case_sensitive = false unless instance_variable_defined?(:@case_sensitive)
23
+ return @case_sensitive unless value
24
+ @case_sensitive = !!value
25
+ end
26
+
27
+ def case_sensitive?
28
+ !!@case_sensitive
29
+ end
30
+
31
+ def compared_attrs
32
+ @compared_attrs ||= []
33
+ @compared_attrs
34
+ end
35
+ end
36
+
37
+ attr_reader :source1, :source2
38
+ attr_reader :src_h1, :src_h2
39
+ attr_reader :logger
40
+
41
+ class_resolver :diff_result_class, "Eco::Data::Hash::DiffResult"
42
+
43
+ def initialize(source1, source2, logger: ::Logger.new(IO::NULL), **kargs)
44
+ @logger = logger
45
+ @options = kargs
46
+ @source1 = source1
47
+ @source2 = source2
48
+ @src_h1 = by_key(source1)
49
+ @src_h2 = by_key(source2)
50
+ raise "Missing source1" unless !!self.src_h1
51
+ raise "Missing source2" unless !!self.src_h2
52
+ end
53
+
54
+ # @note
55
+ # - A `Hash::DiffResult` object, offers `hash_diff` with the attrs that have changed value
56
+ # - It also allows to know the original value
57
+ # @return [Hash] where `key` is the key of the record, and `value` a `DiffResult` object
58
+ def diffs
59
+ @diffs ||= paired_sources.each_with_object([]) do |(src1, src2), diffs|
60
+ args = {
61
+ key: key,
62
+ compare: compared_attrs,
63
+ case_sensitive: case_sensitive?
64
+ }
65
+ diff_result_class.new(src1, src2, **args).tap do |res|
66
+ diffs << res if res.diff?
67
+ end
68
+ end
69
+ end
70
+
71
+ protected
72
+
73
+ # It pairs the hashes of `source1` and `source2`
74
+ # @note
75
+ # - It also ensures they are in their Hash form (with string keys)
76
+ # - This will merge entries of the same source that hold the same `key` attr value (latest wins)
77
+ def paired_sources
78
+ keys1 = src_h1.keys; keys2 = src_h2.keys
79
+ all_keys = keys1 | keys2
80
+ all_keys.map {|key| [src_h1[key], src_h2[key]]}
81
+ end
82
+
83
+ def key
84
+ @key ||= options_or(:key) do
85
+ self.class.key
86
+ end.tap do |k|
87
+ raise "missing main key attr to pair records. Given: #{k}" unless k.is_a?(String)
88
+ end
89
+ end
90
+
91
+ def case_sensitive?
92
+ @case_sensitive ||= options_or(:case_sensitive) { self.class.case_sensitive? }
93
+ end
94
+
95
+ def compared_attrs
96
+ @compared_attrs ||= options_or(:compared_attrs) do
97
+ self.class.compared_attrs
98
+ end.yield_self do |attrs|
99
+ raise "compared_attrs should be an array" unless attrs.is_a?(Array)
100
+ attrs.map(&:to_s)
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def options_or(opt)
107
+ opt = opt.to_sym
108
+ return @options[opt] if @options.key?(opt)
109
+ yield
110
+ end
111
+
112
+ def symbolize_keys(hash)
113
+ hash.each_with_object({}) do |(k, v), h|
114
+ h[k.to_sym] = v
115
+ end
116
+ end
117
+
118
+ def stringify_keys(hash)
119
+ hash.each_with_object({}) do |(k, v), h|
120
+ h[k.to_s] = v
121
+ end
122
+ end
123
+
124
+ def by_key(content)
125
+ to_array_of_hashes(content).each_with_object({}) do |item, out|
126
+ out[item[key]] = item
127
+ end
128
+ end
129
+
130
+ def to_array_of_hashes(content)
131
+ case content
132
+ when Hash
133
+ logger.error("Input data as 'Hash' not supported. Expecting 'Enumerable' or 'String'")
134
+ exit(1)
135
+ when String
136
+ to_array_of_hashes(Eco::CSV.parse(content))
137
+ when Enumerable
138
+ sample = content.to_a.first
139
+ case sample
140
+ when Hash, Array, ::CSV::Row
141
+ Eco::CSV::Table.new(content).to_array_of_hashes
142
+ else
143
+ logger.error("Input content 'Array' of '#{sample.class}' is not supported.")
144
+ exit(1)
145
+ end
146
+ else
147
+ logger.error("Could not obtain any data out content: '#{content.class}'")
148
+ exit(1)
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,103 @@
1
+ module Eco
2
+ module Data
3
+ module Hashes
4
+ class DiffResult
5
+
6
+ attr_reader :key
7
+ attr_reader :src1, :src2
8
+
9
+ # @param [Array<String>, sym]
10
+ # - `:all` compares the matching attrs between both hashes only
11
+ def initialize(src1, src2, key:, compare: :all, case_sensitive: false)
12
+ @key = key
13
+ @compare = compare
14
+ @case_sensitive = case_sensitive
15
+ @src1 = src1
16
+ @src2 = src2
17
+ end
18
+
19
+ def new?
20
+ !src1 && !!src2
21
+ end
22
+
23
+ def del?
24
+ !!src1 && !src2
25
+ end
26
+
27
+ def update?
28
+ !new? && !del? && diff?
29
+ end
30
+
31
+ def diff?
32
+ new? || del? || !diff_attrs.empty?
33
+ end
34
+
35
+ # Is the key attr value changing?
36
+ def key?
37
+ !(new? || del?) && diff_attr?(key)
38
+ end
39
+
40
+ def diff_attr?(attr)
41
+ return true if new?
42
+ return true if del?
43
+ diff_attrs.include?(attr.to_s)
44
+ end
45
+
46
+ def attr(attr)
47
+ return nil unless src2
48
+ src2[attr.to_s]
49
+ end
50
+
51
+ def attr_prev(attr)
52
+ return nil unless src1
53
+ src1[attr.to_s]
54
+ end
55
+
56
+ def previous(attr)
57
+ src1 && src1[attr.to_s]
58
+ end
59
+
60
+ def diff_hash
61
+ target_attrs = [key] | compared_attrs
62
+ return src2.slice(*target_attrs) if new?
63
+ return src1.slice(key) if del?
64
+ src2.slice(key, *diff_attrs)
65
+ end
66
+
67
+ def diff_attrs
68
+ @diff_attrs ||= comparable_attrs.each_with_object([]) do |attr, out|
69
+ out << attr unless eq?(src1[attr], src2[attr])
70
+ end
71
+ end
72
+
73
+ def eq?(val1, val2)
74
+ return true if val1 == val2
75
+ return false if case_sensitive?
76
+ return false if !val2 || !val1
77
+ val1.upcase == val2.upcase
78
+ end
79
+
80
+ def case_sensitive?
81
+ !!@case_sensitive
82
+ end
83
+
84
+ def comparable_attrs
85
+ return [] if new? || del?
86
+ compared_attrs
87
+ end
88
+
89
+ def compared_attrs
90
+ return @compared_attrs if instance_variable_defined?(:@compared_attrs)
91
+ @compared_attrs = \
92
+ if @compare == :all
93
+ src1.keys & src2.keys
94
+ elsif @compare.is_a?(Array)
95
+ @compare.map(&:to_s)
96
+ else
97
+ raise "Expecting 'compare' to be sym (:all) or Array<String>. Given: #{@compare.class}"
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,9 @@
1
+ module Eco
2
+ module Data
3
+ module Hashes
4
+ end
5
+ end
6
+ end
7
+
8
+ require_relative 'hashes/diff_result'
9
+ require_relative 'hashes/array_diff'
data/lib/eco/data.rb CHANGED
@@ -7,3 +7,4 @@ require_relative 'data/crypto'
7
7
  require_relative 'data/files'
8
8
  require_relative 'data/mapper'
9
9
  require_relative 'data/fuzzy_match'
10
+ require_relative 'data/hashes'
@@ -0,0 +1,19 @@
1
+ module Eco
2
+ module Language
3
+ # Some modules/classes use logger, but they may not be connected to session.
4
+ # This prevents errors with this.
5
+ module AuxiliarLogger
6
+ def logger
7
+ if defined?(super)
8
+ super
9
+ elsif respond_to?(:session)
10
+ session.logger
11
+ elsif instance_variable_defined?(:@session)
12
+ @session.logger
13
+ else
14
+ @logger ||= ::Logger.new(IO::NULL)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,134 @@
1
+ module Eco
2
+ module Language
3
+ module Models
4
+ module ClassHelpers
5
+ NOT_USED = "no_used!"
6
+
7
+ def redef_without_warning(const, value)
8
+ self.class.send(:remove_const, const) if self.class.const_defined?(const)
9
+ self.class.const_set(const, value)
10
+ end
11
+
12
+ def class_resolver(name, klass)
13
+ define_singleton_method(name) { resolve_class(klass) }
14
+ define_method(name) { self.class.resolve_class(klass) }
15
+ end
16
+
17
+ # Class resolver
18
+ # @note it caches the resolved `klass`es
19
+ # @raise [Exception] when could not resolve if `exception` is `true`
20
+ # @param klass [Class, String, Symbol] the class to resolve
21
+ # @param source_class [Class] when the reference to `klass` belongs to a different inheritance chain.
22
+ # @param exception [Boolean] if it should raise exception when could not resolve
23
+ # @return [Class] the `Class` constant
24
+ def resolve_class(klass, source_class: self, exception: true)
25
+ @resolved ||= {}
26
+ @resolved[klass] ||=
27
+ case klass
28
+ when Class
29
+ klass
30
+ when String
31
+ begin
32
+ Kernel.const_get(klass)
33
+ rescue NameError => e
34
+ raise if exception
35
+ end
36
+ when Symbol
37
+ source_class.resolve_class(source_class.send(klass))
38
+ when Hash
39
+ referrer, referred = klass.first
40
+ resolve_class(referred, source_class: referrer, exception: exception)
41
+ else
42
+ raise "Unknown class: #{klass}" if exception
43
+ end
44
+ end
45
+
46
+ # Helper to normalize `key` into a correct `ruby` **constant name**
47
+ # @note it removes namespace syntax `::`
48
+ # @param key [String, Symbol] to be normalized
49
+ # @return [String] a correct constant name
50
+ def to_constant(key)
51
+ str_name = key.to_s.strip.split(/::/).compact.map do |str|
52
+ str.slice(0).upcase + str.slice(1..-1)
53
+ end.join("").split(/[\-\_ :]+/i).compact.map do |str|
54
+ str.slice(0).upcase + str.slice(1..-1)
55
+ end.join("")
56
+ end
57
+
58
+ # Helper to create an instance variable `name`
59
+ # @param [String, Symbol] the name of the variable
60
+ # @reutrn [String] the name of the created instance variable
61
+ def instance_variable_name(name)
62
+ str = name.to_s
63
+ str = "@#{str}" unless str.start_with?("@")
64
+ str
65
+ end
66
+
67
+ # If the class for `name` exists, it returns it. Otherwise it generates it.
68
+ # @param name [String, Symbol] the name of the new class
69
+ # @param inherits [Class] the parent class to _inherit_ from
70
+ # @param namespace [Class, String] an existing `constant` (class or module) the new class will be namespaced on
71
+ # @yield [child_class] configure the new class
72
+ # @yieldparam child_class [Class] the new class
73
+ # @return [Class] the new generated class
74
+ def new_class(name = "Child#{uid}", inherits: self, namespace: inherits)
75
+ name = name.to_s.to_sym.freeze
76
+ class_name = to_constant(name)
77
+
78
+ unless target_class = resolve_class("#{namespace}::#{class_name}", exception: false)
79
+ target_class = Class.new(inherits)
80
+ Kernel.const_get(namespace.to_s).const_set class_name, target_class
81
+ end
82
+
83
+ target_class.tap do |klass|
84
+ yield(klass) if block_given?
85
+ end
86
+ end
87
+
88
+ # Helper to determine if a paramter has been used
89
+ # @note to effectivelly use this helper, you should initialize your target
90
+ # paramters with the constant `NOT_USED`
91
+ # @param val [] the value of the paramter
92
+ # @return [Boolean] `true` if value other than `NOT_USED`, `false` otherwise
93
+ def used_param?(val)
94
+ val != NOT_USED
95
+ end
96
+
97
+ # Keeps track on class instance variables that should be inherited by child classes.
98
+ # @note
99
+ # - subclasses will inherit the value as is at that moment
100
+ # - any change afterwards will be only on the specific class (in line with class instance variables)
101
+ # - adapted from https://stackoverflow.com/a/10729812/4352306
102
+ # TODO: this separates the logic of the method to the instance var. Think if would be possible to join them somehow.
103
+ def inheritable_class_vars(*vars)
104
+ @inheritable_class_vars ||= [:inheritable_class_vars]
105
+ @inheritable_class_vars += vars
106
+ end
107
+
108
+ # Builds the attr_reader and attr_writer of `attrs` and registers the associated instance variable as inheritable.
109
+ def inheritable_attrs(*attrs)
110
+ attrs.each do |attr|
111
+ class_eval %(
112
+ class << self; attr_accessor :#{attr} end
113
+ )
114
+ end
115
+ inheritable_class_vars(*attrs)
116
+ end
117
+
118
+ # This callback method is called whenever a subclass of the current class is created.
119
+ # @note
120
+ # - values of the instance variables are copied as they are (no dups or clones)
121
+ # - the above means: avoid methods that change the state of the mutable object on it
122
+ # - mutating methods would reflect the changes on other classes as well
123
+ # - therefore, `freeze` will be called on the values that are inherited.
124
+ def inherited(subclass)
125
+ inheritable_class_vars.each do |var|
126
+ instance_var = instance_variable_name(var)
127
+ value = instance_variable_get(instance_var)
128
+ subclass.instance_variable_set(instance_var, value.freeze)
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -5,6 +5,7 @@ module Eco
5
5
  end
6
6
  end
7
7
 
8
+ require_relative 'models/class_helpers'
8
9
  require_relative 'models/modifier'
9
10
  require_relative 'models/collection'
10
11
  require_relative 'models/parser_serializer'
data/lib/eco/language.rb CHANGED
@@ -10,3 +10,4 @@ require_relative 'language/hash_transform'
10
10
  require_relative 'language/values_at'
11
11
  require_relative 'language/match_modifier'
12
12
  require_relative 'language/match'
13
+ require_relative 'language/auxiliar_logger'
data/lib/eco/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Eco
2
- VERSION = "2.4.3"
2
+ VERSION = "2.4.5"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eco-helpers
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.3
4
+ version: 2.4.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oscar Segura
@@ -591,6 +591,9 @@ files:
591
591
  - lib/eco/api/usecases/default_cases/upsert_case.rb
592
592
  - lib/eco/api/usecases/graphql.rb
593
593
  - lib/eco/api/usecases/graphql/base.rb
594
+ - lib/eco/api/usecases/graphql/helpers.rb
595
+ - lib/eco/api/usecases/graphql/helpers/locations.rb
596
+ - lib/eco/api/usecases/graphql/helpers/locations/commands.rb
594
597
  - lib/eco/api/usecases/ooze_cases.rb
595
598
  - lib/eco/api/usecases/ooze_cases/export_register_case.rb
596
599
  - lib/eco/api/usecases/ooze_samples.rb
@@ -657,14 +660,19 @@ files:
657
660
  - lib/eco/data/fuzzy_match/score.rb
658
661
  - lib/eco/data/fuzzy_match/stop_words.rb
659
662
  - lib/eco/data/fuzzy_match/string_helpers.rb
663
+ - lib/eco/data/hashes.rb
664
+ - lib/eco/data/hashes/array_diff.rb
665
+ - lib/eco/data/hashes/diff_result.rb
660
666
  - lib/eco/data/mapper.rb
661
667
  - lib/eco/language.rb
668
+ - lib/eco/language/auxiliar_logger.rb
662
669
  - lib/eco/language/curry.rb
663
670
  - lib/eco/language/hash_transform.rb
664
671
  - lib/eco/language/hash_transform_modifier.rb
665
672
  - lib/eco/language/match.rb
666
673
  - lib/eco/language/match_modifier.rb
667
674
  - lib/eco/language/models.rb
675
+ - lib/eco/language/models/class_helpers.rb
668
676
  - lib/eco/language/models/collection.rb
669
677
  - lib/eco/language/models/modifier.rb
670
678
  - lib/eco/language/models/parser_serializer.rb