ecoportal-api-v2 1.1.6 → 1.1.7

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: b6294103b68ebdbf099d1137865856b10baec21683bd648727976f245cf197ad
4
- data.tar.gz: e0500e2c784a46c7a82ca3aa29e44f96d116bd6898e5b35f9174162457208165
3
+ metadata.gz: 65194be30f026aa431874543d6905b5704ea081c6a3c9ef0bf9741142c4c4253
4
+ data.tar.gz: 398eaeb83ea38386670715c0fa83144004abdbd8c7edfd8581ecb7b157c8d205
5
5
  SHA512:
6
- metadata.gz: eb18fb2a167afeaea5eac0db4e13ee58d64fc348ff1ebd33942ad1bb7120cbb828ad2652ed2d6b165d7a1b53eea097338032690e26ee97f5765fd87ba2f97862
7
- data.tar.gz: 7a61a35335247d39a0e97574f5b2816a92238d93a9f5b23c62b909a92ae03817057752ffc576ae1414c5e6744a6381b473be4cd2cfc2a0fd4e7582cffd97bdd4
6
+ metadata.gz: 0761f2079d2493f6fef928f846daf4b6b0413838c4f1252c8cf31742284fc5bcfc0e8d6dbd635f2659b37a34a6b90c09ed42f7db781c868a635188354dbe4bbd
7
+ data.tar.gz: c979ae5a583898d47c1b8501d19ea651f31013a78461008c4ed6eff69c6b1a271c2c812dbbc2a52bc82d90b7caf67e9a286e8d488639eb8bd5a8fd6678c91659
data/CHANGELOG.md CHANGED
@@ -1,12 +1,24 @@
1
1
  # Change Log
2
2
  All notable changes to this project will be documented in this file.
3
3
 
4
- ## [1.1.7] - 2024-02-xx
4
+ ## [1.1.8] - 2024-03-xx
5
5
 
6
6
  ### Added
7
7
  ### Changed
8
8
  ### Fixed
9
9
 
10
+ ## [1.1.7] - 2024-03-xx
11
+
12
+ ### Added
13
+ - `Ecoportal::API::Common::Concerns::Benchmarkable`
14
+ - `Ecoportal::API::Common::Conent::CollectionModel`
15
+ - added ability to define **default** value for `read_only`
16
+
17
+ ### Fixed
18
+ - `Ecoportal::API::Common::Content::CollectionModel` tidied up item `klass` scoping
19
+ - **added** some benchmarking too
20
+ - memoize `@_items`
21
+
10
22
  ## [1.1.6] - 2024-02-29
11
23
 
12
24
  ### Added
@@ -0,0 +1,160 @@
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ module Concerns
5
+ # @note While benchmarking with multi-thread gives correct results
6
+ # for top unique block, will calculate the real time of each individual
7
+ # thread, which is way higher than a single thread. However, as they
8
+ # run in parallel, the total time would be the valid reference.
9
+ module Benchmarkable
10
+ private
11
+
12
+ def benchmark_enabled?
13
+ @benchmark_enabled = true if @benchmark_enabled.nil?
14
+ @benchmark_enabled
15
+ end
16
+
17
+ def benchmarking(ref = nil, print: false)
18
+ return yield unless benchmark_enabled?
19
+ benchmark_mem(ref, print: false) do
20
+ benchmark_time(ref, print: false) do
21
+ yield
22
+ end
23
+ end.tap do
24
+ puts "\n#{bench_summary_ref(ref)}" if print
25
+ end
26
+ end
27
+
28
+ def benchmark_mem(ref = nil, print: false)
29
+ return yield unless benchmark_enabled?
30
+ memory_before = memory_usage
31
+ yield.tap do
32
+ memory_after = memory_usage
33
+ mb_footprint = ((memory_after - memory_before) / 1024.0).round(2)
34
+ bench_add_mem(ref, mb_footprint)
35
+ puts bench_str_mem(mb, ref: ref, reffix: true) if print
36
+ end
37
+ end
38
+
39
+ def benchmark_time(ref = nil, print: false)
40
+ return yield unless benchmark_enabled?
41
+ result = nil
42
+ time = benchmark.realtime do
43
+ result = yield
44
+ end.round(2)
45
+ result.tap do
46
+ bench_add_time(ref, time)
47
+ puts bench_str_time(time, ref: ref, reffix: true) if print
48
+ end
49
+ end
50
+
51
+ def benchmark_summary(ref = nil)
52
+ return '' unless benchmark_enabled?
53
+ return bench_summary_ref(ref) unless ref == :all
54
+
55
+ bench.keys.each_with_object([]) do |ref, out|
56
+ out << bench_summary_ref(ref)
57
+ end.join("\n")
58
+ end
59
+
60
+ def bench_summary_ref(ref = nil)
61
+ ref_lines = bench_summary_ref_lines(ref)
62
+ "- #{ref}\n" + " • " + ref_lines.join("\n • ")
63
+ end
64
+
65
+ def bench_summary_ref_lines(ref = nil)
66
+ ref_bench = bench_get(ref)
67
+ [
68
+ bench_str_time(*ref_bench[:time].values_at(:avg, :cnt), ref: ref),
69
+ bench_str_mem(*ref_bench[:mem].values_at(:avg, :cnt), ref: ref)
70
+ ]
71
+ end
72
+
73
+ def benchmark
74
+ require 'benchmark'
75
+ Benchmark
76
+ end
77
+
78
+ def bench
79
+ @bench ||= {}
80
+ end
81
+
82
+ def bench_add_mem(ref, mem)
83
+ bench_data_push(bench_get(ref)[:mem], mem)
84
+ end
85
+
86
+ def bench_add_time(ref, time)
87
+ bench_data_push(bench_get(ref)[:time], time)
88
+ end
89
+
90
+ def bench_get(ref)
91
+ bench[ref] ||= {
92
+ time: bench_data,
93
+ mem: bench_data
94
+ }
95
+ end
96
+
97
+ def bench_data
98
+ {
99
+ avg: nil,
100
+ cnt: 0
101
+ }
102
+ end
103
+
104
+ def bench_data_push(data, value)
105
+ total = value + ( (data[:avg] || 0) * data[:cnt])
106
+ data[:cnt] += 1
107
+ data[:avg] = (total / data[:cnt]).round(3)
108
+ data
109
+ end
110
+
111
+ def bench_str_mem(mem, count = nil, ref: nil, reffix: false)
112
+ ref = reffix ? ref : nil
113
+ msg = [ref, 'Memory'].compact.join(' -- ')
114
+ cnt = count ? " (cnt: #{count})" : ''
115
+ "#{msg}: #{mem} MB#{cnt}"
116
+ end
117
+
118
+ def active_support_duration?
119
+ return false unless Kernel.const_defined?(:ActiveSupport)
120
+ ActiveSupport.const_defined?(:Duration, false)
121
+ end
122
+
123
+ def bench_str_time(time, count = nil, ref: nil, reffix: false)
124
+ ref = reffix ? ref : nil
125
+ msg = [ref, 'Time'].compact.join(' -- ')
126
+ total = (time * count).round(2)
127
+ str_desc = ''
128
+ if active_support_duration? && total >= 60
129
+ duration = ActiveSupport::Duration.build(total.round)
130
+ str_desc = ": #{duration_to_s(duration)}"
131
+ end
132
+ cnt = count ? " (cnt: #{count}; sum: #{total} s#{str_desc})" : ''
133
+ "#{msg}: #{time} s#{cnt}"
134
+ end
135
+
136
+ def duration_to_s(value)
137
+ return "" unless active_support_duration?
138
+ return "" if value.nil?
139
+
140
+ raise ArgumentError, "Expecint ActiveSupport::Duration. Given: #{value.class}" unless value.is_a?(ActiveSupport::Duration)
141
+ parts = value.parts.map {|pair| pair.reverse.join(" ")}
142
+ return parts.first if parts.length == 1
143
+ [parts[..-2].join(", "), parts.last].join(" and ")
144
+ end
145
+
146
+ # @return [Integer] total memory used by this process in KB
147
+ def memory_usage
148
+ if Gem.win_platform?
149
+ wmem = `wmic process where processid=#{Process.pid} get WorkingSetSize | findstr "[0-9]"`
150
+ return 0 unless wmem
151
+ wmem.lines.first.chop.strip.to_i / 1024.0
152
+ else
153
+ `ps -o rss= -p #{Process.pid}`.to_i
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,10 @@
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ module Concerns
5
+ end
6
+ end
7
+ end
8
+ end
9
+
10
+ require 'ecoportal/api/common/concerns/benchmarkable'
@@ -34,7 +34,7 @@ module Ecoportal
34
34
 
35
35
  inheritable_class_vars :order_matteres, :uniq
36
36
 
37
- def initialize(doc = [], parent: self, key: nil, read_only: false)
37
+ def initialize(doc = [], parent: self, key: nil, read_only: self.class.read_only?)
38
38
  super(doc, parent: parent, key: key, read_only: read_only)
39
39
  end
40
40
 
@@ -6,7 +6,6 @@ module Ecoportal
6
6
  # @note to be able to refer to the correct element of the Collection,
7
7
  # it is required that those elements have a unique `key` that allows to identify them
8
8
  class CollectionModel < Content::DoubleModel
9
-
10
9
  class << self
11
10
  attr_writer :klass
12
11
  attr_accessor :order_matters, :order_key
@@ -26,24 +25,24 @@ module Ecoportal
26
25
  # - use block to define `klass` callback
27
26
  # @note When `klass` is resolved, if the items are of type
28
27
  # `DoubleModel`, it sets on the collection class the `items_key`
28
+ # @note when `klass` is directly resolved (not via doc) only once
29
+ # it will set @klass as resolved and will use this class from now on.
30
+ # This is an optimization to cut class lookups
29
31
  # @param value [Hash] base `doc` (raw object) to create the object with
30
32
  # @yield [doc] identifies the target `class` of the raw object
31
33
  # @yieldparam doc [Hash]
32
34
  # @yieldreturn [Klass] the target `class`
33
35
  # @return [Klass] the target `class`
34
36
  def klass(value = NOT_USED, &block)
35
- if block
36
- @klass = block
37
- block.call(value) if value != NOT_USED
37
+ @klass = block if block_given?
38
+
39
+ if @klass && !@class.is_a?(Proc)
40
+ @klass = resolve_class(@klass, exception: false) unless @klass.is_a?(Class)
38
41
  @klass
39
- elsif used_param?(value)
40
- if @klass.is_a?(Proc)
41
- @klass.call(value)
42
- else
43
- resolve_class(@klass, exception: false)
44
- end
42
+ elsif @klass.is_a?(Proc) && used_param?(value)
43
+ @klass.call(value)
45
44
  else
46
- resolve_class(@klass, exception: false)
45
+ @klass
47
46
  end.tap do |result|
48
47
  next unless result.is_a?(Class)
49
48
  next unless result < Ecoportal::API::Common::Content::DoubleModel
@@ -51,6 +50,19 @@ module Ecoportal
51
50
  end
52
51
  end
53
52
 
53
+ # @return [Boolean] are there the factory logics to build item objects defined?
54
+ def klass?
55
+ @klass || @new_item
56
+ end
57
+
58
+ # Optimization
59
+ def new_item_class_based?
60
+ return false if @new_item.is_a?(Proc)
61
+ return false if @klass.is_a?(Proc)
62
+ return true if klass.is_a?(Class)
63
+ false
64
+ end
65
+
54
66
  # Generates a new object of the target class
55
67
  # @note
56
68
  # - use block to define `new_item` callback, which will prevail over `klass`
@@ -60,29 +72,24 @@ module Ecoportal
60
72
  # @yield [doc, parent, key] creates an object instance of the target `klass`
61
73
  # @yieldparam doc [Hash]
62
74
  # @yieldreturn [Klass] instance object of the target `klass`
75
+ # @parent [CollectionModel] the parent of the new item
76
+ # @key [Symbol, String] the key value to access the item within collection
77
+ # Please observe that items in a CollectionModel are identified via their key attr.
78
+ # Meaning that there is actually no need to define this argument.
63
79
  # @return [Klass] instance object of the target `klass`
64
80
  def new_item(doc = NOT_USED, parent: nil, key: nil, read_only: false, &block)
65
- if block
66
- @new_item = block
67
- elsif used_param?(doc)
68
- raise "You should define either a 'klass' or a 'new_item' callback first" unless klass?
69
- if @new_item
70
- @new_item.call(doc, parent, key)
71
- else
72
- if target_class = self.klass(doc)
73
- doc.is_a?(target_class) ? doc : target_class.new(doc, parent: parent, key: key, read_only: read_only)
74
- else
75
- raise "Could not find a class for: #{doc}"
76
- end
77
- end
78
- else
79
- raise "To define the 'new_item' callback (factory), you need to use a block"
80
- end
81
- end
81
+ return (@new_item = block; nil) if block_given
82
82
 
83
- # @return [Boolean] are there the factory logics to build item objects defined?
84
- def klass?
85
- @klass || @new_item
83
+ msg = "To define the 'new_item' callback (factory), you need to use a block"
84
+ raise msg unless used_param?(doc)
85
+ msg = "You should define either a 'klass' or a 'new_item' callback first"
86
+ raise msg unless klass?
87
+ return @new_item.call(doc, parent, key) if @new_item.is_a?(Proc)
88
+
89
+ raise "Could not find a class for: #{doc}" unless target_class = klass(doc)
90
+ return doc if doc.is_a?(target_class)
91
+
92
+ target_class.new(doc, parent: parent, key: key, read_only: read_only)
86
93
  end
87
94
 
88
95
  def doc_class(name)
@@ -166,6 +173,7 @@ module Ecoportal
166
173
  _doc_items.each do |item_doc|
167
174
  elements << new_item(item_doc)
168
175
  end
176
+ @_items = elements if read_only?
169
177
  end
170
178
  end
171
179
 
@@ -275,7 +283,11 @@ module Ecoportal
275
283
  private
276
284
 
277
285
  def new_item(value)
278
- self.class.new_item(value, parent: self, read_only: self._read_only)
286
+ if self.class.new_item_class_based?
287
+ self.class.klass.new(value, parent: self, read_only: self._read_only)
288
+ else
289
+ self.class.new_item(value, parent: self, read_only: self._read_only)
290
+ end
279
291
  end
280
292
 
281
293
  # Helper to remove tracked down instance variables
@@ -38,6 +38,16 @@ module Ecoportal
38
38
  uid(length)
39
39
  end
40
40
 
41
+ def read_only?
42
+ @read_only = false if @read_only.nil?
43
+ @read_only
44
+ end
45
+
46
+ # Be able to define if a class should be read-only
47
+ def read_only!
48
+ @read_only = true
49
+ end
50
+
41
51
  # Same as `attr_reader` but links to a subjacent `Hash` model property
42
52
  # @note it does **not** create an _instance variable_
43
53
  # @param methods [Array<Symbol>] the method that exposes the value
@@ -176,7 +186,7 @@ module Ecoportal
176
186
 
177
187
  define_method method do
178
188
  return instance_variable_get(var) if instance_variable_defined?(var)
179
- new_obj = dim_class.new(parent: self, key: method, read_only: self._read_only)
189
+ new_obj = dim_class.new(parent: self, key: method, read_only: self.read_only?)
180
190
  variable_set(var, new_obj)
181
191
  end
182
192
  end
@@ -202,7 +212,7 @@ module Ecoportal
202
212
  # @param read_only [Boolean] whether or not should try to **work around** items `klass` missing a `key`
203
213
  # - If set to `true` this is meant only for read purposes (won't be able to successufully insert)
204
214
  def embeds_many(method, key: method, klass: nil, enum_class: nil,
205
- order_matters: false, order_key: nil, read_only: false)
215
+ order_matters: false, order_key: nil, read_only: self.read_only?)
206
216
  if enum_class
207
217
  eclass = enum_class
208
218
  elsif klass
@@ -210,6 +220,7 @@ module Ecoportal
210
220
  dim_class.klass = klass
211
221
  dim_class.order_matters = order_matters
212
222
  dim_class.order_key = order_key
223
+ dim_class.read_only! if read_only
213
224
  end
214
225
  else
215
226
  raise "You should either specify the 'klass' of the elements or the 'enum_class'"
@@ -224,7 +235,7 @@ module Ecoportal
224
235
 
225
236
  private
226
237
 
227
- def embed(method, key: method, nullable: false, multiple: false, klass:, read_only: false, &block)
238
+ def embed(method, key: method, nullable: false, multiple: false, klass:, read_only: self.read_only?, &block)
228
239
  method = method.to_s.freeze
229
240
  var = instance_variable_name(method).freeze
230
241
  k = key.to_s.freeze
@@ -251,7 +262,7 @@ module Ecoportal
251
262
  end
252
263
  end
253
264
 
254
- embedded_class.new(doc[k], parent: self, key: k, read_only: self._read_only || read_only).tap do |obj|
265
+ embedded_class.new(doc[k], parent: self, key: k, read_only: self.read_only? || read_only).tap do |obj|
255
266
  variable_set(var, obj)
256
267
  end
257
268
  end
@@ -263,12 +274,12 @@ module Ecoportal
263
274
  end
264
275
  end
265
276
 
266
- inheritable_class_vars :forced_model_keys, :key
277
+ inheritable_class_vars :forced_model_keys, :key, :read_only
267
278
 
268
279
  # `_key` refers to the parent's property that links to this model
269
280
  attr_reader :_parent, :_key, :_read_only
270
281
 
271
- def initialize(doc = {}, parent: self, key: nil, read_only: false)
282
+ def initialize(doc = {}, parent: self, key: nil, read_only: self.class.read_only?)
272
283
  @_dim_vars = []
273
284
  @_parent = parent || self
274
285
  @_key = key || self
@@ -287,6 +298,12 @@ module Ecoportal
287
298
  end
288
299
  end
289
300
 
301
+ # @note `read_only` allows for some optimizations, such as storing values
302
+ # in instance variables, for optimization purposes
303
+ def read_only?
304
+ @_read_only
305
+ end
306
+
290
307
  def root
291
308
  return self if is_root?
292
309
  _parent.root
@@ -408,6 +425,9 @@ module Ecoportal
408
425
  !!defined?(@doc)
409
426
  end
410
427
 
428
+ # Both requisites
429
+ # @note that for optimization purposes, `@doc` var may be used when
430
+ # the object is `read_only?`
411
431
  def is_root?
412
432
  _parent == self && doc_var?
413
433
  end
@@ -5,4 +5,5 @@ module Ecoportal
5
5
  end
6
6
  end
7
7
 
8
+ require 'ecoportal/api/common/concerns'
8
9
  require 'ecoportal/api/common/content'
@@ -1,5 +1,5 @@
1
1
  module Ecoportal
2
2
  module API
3
- GEM2_VERSION = "1.1.6"
3
+ GEM2_VERSION = "1.1.7"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ecoportal-api-v2
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.6
4
+ version: 1.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oscar Segura
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-02-29 00:00:00.000000000 Z
11
+ date: 2024-03-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -167,6 +167,8 @@ files:
167
167
  - ecoportal-api-v2.gemspec
168
168
  - lib/ecoportal/api-v2.rb
169
169
  - lib/ecoportal/api/common.v2.rb
170
+ - lib/ecoportal/api/common/concerns.rb
171
+ - lib/ecoportal/api/common/concerns/benchmarkable.rb
170
172
  - lib/ecoportal/api/common/content.rb
171
173
  - lib/ecoportal/api/common/content/array_model.rb
172
174
  - lib/ecoportal/api/common/content/class_helpers.rb