eco-helpers 2.4.4 → 2.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c2d5e728987bee135ac2297b0a84858dfb14eab79b3decebc59ced1a74ccc4a
4
- data.tar.gz: b906c449d80dbc1440c1b7e7d91565bad53409167cae3c07b0788870f842cf5e
3
+ metadata.gz: 333d58d63c6ffad6b06865c89dce3e48a50347ba998361f34eb9a2e230ea83f2
4
+ data.tar.gz: 7be152e20c00765217b6650790dfd4aeba729bb614e6d0928a7e7ea82d94de33
5
5
  SHA512:
6
- metadata.gz: e7a9781490f01e1d9ffafdec19179d492d760c9dca676e2e6f2771b61a8f422d5c9f481bb03c71563c91df97b72f89cee0e8aa664be42dcf1dd5d99576177add
7
- data.tar.gz: 44eed6e779b8ffdfdc1bea5913090713cc15dba649b7c2eddc9f333a8be67f92afa1ec6e1d06b0c31bcf2b29d062b92cac26c7c53b07404e50d089ae56698202
6
+ metadata.gz: 3cb79f7d5429bf4c3cd8b8b37cbbdb2b8208da2161559fb6a09b08a67cea1ded2d08355928af4ee136b39bc10fda3f82ece275f5991c65bfcf41c4290d7d7269
7
+ data.tar.gz: 78723001238f02cac999e2af5200e1e13f444572366291357a70ec951732ded65bd708be9b6c0259a9e6bf797fc5c803df17a28167d01b1bf1223900f8226cc7
data/CHANGELOG.md CHANGED
@@ -1,12 +1,27 @@
1
1
  # Change Log
2
2
  All notable changes to this project will be documented in this file.
3
3
 
4
- ## [2.4.5] - 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
+
10
25
  ## [2.4.4] - 2023-03-29
11
26
 
12
27
  ### Added
@@ -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,7 +5,7 @@ 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
10
  attr_reader :parent
11
11
  attr_reader :nodes, :children_count
@@ -24,7 +24,7 @@ module Eco
24
24
  # ]}]
25
25
  # tree = TagTree.new(tree.to_json)
26
26
  # @param tagtree [String] representation of the tagtree in json.
27
- def initialize(tagtree = [], name: nil, id: nil, depth: -1, path: [], parent: nil, enviro: nil)
27
+ def initialize(tagtree = [], name: nil, id: nil, depth: -1, path: [], parent: nil, _weight: nil, enviro: nil)
28
28
  @depth = depth
29
29
  @parent = parent
30
30
 
@@ -39,25 +39,31 @@ module Eco
39
39
  @enviro = enviro
40
40
 
41
41
  if @source.is_a?(Array)
42
- @id = id
43
- @name = name
42
+ @id = id
43
+ @name = name
44
44
  @row_nodes = @source
45
45
  else
46
- @id = @source.values_at('tag', 'id').compact.first&.upcase
47
- @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)
48
50
  @row_nodes = @source['nodes'] || []
49
51
  end
50
52
 
51
53
  @path = path || []
52
54
  @path.push(@id) unless top?
53
55
 
54
- @nodes = @row_nodes.map do |cnode|
55
- TagTree.new(cnode, depth: depth + 1, path: @path.dup, parent: self, 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)
56
58
  end
57
59
 
58
60
  init_hashes
59
61
  end
60
62
 
63
+ def archived?
64
+ @archived
65
+ end
66
+
61
67
  # @return [Eco::API::Organization::TagTree]
62
68
  def dup
63
69
  self.class.new(as_json)
@@ -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,7 +45,7 @@ module Eco
45
45
  end
46
46
 
47
47
  # @see Eco::API::Session::Config#live_tree
48
- def live_tree
48
+ def live_tree(include_archived: false)
49
49
  config.live_tree(enviro: enviro)
50
50
  end
51
51
 
@@ -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'
@@ -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.4"
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.4
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