ecoportal-api-v2 0.8.4

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.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +55 -0
  5. data/.travis.yml +5 -0
  6. data/.yardopts +10 -0
  7. data/CHANGELOG.md +171 -0
  8. data/Gemfile +6 -0
  9. data/LICENSE +21 -0
  10. data/README.md +22 -0
  11. data/Rakefile +27 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/ecoportal-api-v2.gemspec +34 -0
  15. data/lib/ecoportal/api-v2.rb +10 -0
  16. data/lib/ecoportal/api/common.rb +18 -0
  17. data/lib/ecoportal/api/common/content.rb +18 -0
  18. data/lib/ecoportal/api/common/content/array_model.rb +286 -0
  19. data/lib/ecoportal/api/common/content/class_helpers.rb +146 -0
  20. data/lib/ecoportal/api/common/content/client.rb +40 -0
  21. data/lib/ecoportal/api/common/content/collection_model.rb +279 -0
  22. data/lib/ecoportal/api/common/content/doc_helpers.rb +67 -0
  23. data/lib/ecoportal/api/common/content/double_model.rb +356 -0
  24. data/lib/ecoportal/api/common/content/hash_diff_patch.rb +183 -0
  25. data/lib/ecoportal/api/common/content/string_digest.rb +27 -0
  26. data/lib/ecoportal/api/common/content/wrapped_response.rb +42 -0
  27. data/lib/ecoportal/api/v2.rb +82 -0
  28. data/lib/ecoportal/api/v2/page.rb +42 -0
  29. data/lib/ecoportal/api/v2/page/component.rb +133 -0
  30. data/lib/ecoportal/api/v2/page/component/action.rb +28 -0
  31. data/lib/ecoportal/api/v2/page/component/action_field.rb +54 -0
  32. data/lib/ecoportal/api/v2/page/component/chart_field.rb +54 -0
  33. data/lib/ecoportal/api/v2/page/component/chart_field/frequency.rb +29 -0
  34. data/lib/ecoportal/api/v2/page/component/chart_field/heatmap.rb +27 -0
  35. data/lib/ecoportal/api/v2/page/component/chart_field/indicator.rb +26 -0
  36. data/lib/ecoportal/api/v2/page/component/chart_field/multiseries.rb +31 -0
  37. data/lib/ecoportal/api/v2/page/component/chart_field/sankey.rb +27 -0
  38. data/lib/ecoportal/api/v2/page/component/chart_field/serie.rb +26 -0
  39. data/lib/ecoportal/api/v2/page/component/chart_field/series_config.rb +23 -0
  40. data/lib/ecoportal/api/v2/page/component/chart_fr_field.rb +32 -0
  41. data/lib/ecoportal/api/v2/page/component/checklist_field.rb +49 -0
  42. data/lib/ecoportal/api/v2/page/component/checklist_item.rb +25 -0
  43. data/lib/ecoportal/api/v2/page/component/date_field.rb +34 -0
  44. data/lib/ecoportal/api/v2/page/component/file.rb +16 -0
  45. data/lib/ecoportal/api/v2/page/component/files_field.rb +13 -0
  46. data/lib/ecoportal/api/v2/page/component/gauge_field.rb +36 -0
  47. data/lib/ecoportal/api/v2/page/component/gauge_stop.rb +88 -0
  48. data/lib/ecoportal/api/v2/page/component/geo_field.rb +13 -0
  49. data/lib/ecoportal/api/v2/page/component/image.rb +16 -0
  50. data/lib/ecoportal/api/v2/page/component/images_field.rb +23 -0
  51. data/lib/ecoportal/api/v2/page/component/law_field.rb +12 -0
  52. data/lib/ecoportal/api/v2/page/component/number_field.rb +12 -0
  53. data/lib/ecoportal/api/v2/page/component/people_field.rb +25 -0
  54. data/lib/ecoportal/api/v2/page/component/plain_text_field.rb +15 -0
  55. data/lib/ecoportal/api/v2/page/component/reference_field.rb +16 -0
  56. data/lib/ecoportal/api/v2/page/component/rich_text_field.rb +13 -0
  57. data/lib/ecoportal/api/v2/page/component/selection_field.rb +78 -0
  58. data/lib/ecoportal/api/v2/page/component/selection_option.rb +25 -0
  59. data/lib/ecoportal/api/v2/page/component/signature_field.rb +25 -0
  60. data/lib/ecoportal/api/v2/page/component/tag_field.rb +14 -0
  61. data/lib/ecoportal/api/v2/page/components.rb +42 -0
  62. data/lib/ecoportal/api/v2/page/section.rb +59 -0
  63. data/lib/ecoportal/api/v2/page/sections.rb +47 -0
  64. data/lib/ecoportal/api/v2/page/stage.rb +29 -0
  65. data/lib/ecoportal/api/v2/page/stages.rb +26 -0
  66. data/lib/ecoportal/api/v2/pages.rb +92 -0
  67. data/lib/ecoportal/api/v2/pages/page_stage.rb +16 -0
  68. data/lib/ecoportal/api/v2/pages/stages.rb +55 -0
  69. data/lib/ecoportal/api/v2/people.rb +31 -0
  70. data/lib/ecoportal/api/v2/registers.rb +89 -0
  71. data/lib/ecoportal/api/v2/registers/page_result.rb +21 -0
  72. data/lib/ecoportal/api/v2/registers/register.rb +37 -0
  73. data/lib/ecoportal/api/v2/registers/stage_result.rb +14 -0
  74. data/lib/ecoportal/api/v2/registers/stages_result.rb +13 -0
  75. data/lib/ecoportal/api/v2/registers/template.rb +12 -0
  76. data/lib/ecoportal/api/v2/version.rb +7 -0
  77. metadata +254 -0
@@ -0,0 +1,40 @@
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ module Content
5
+ # @see Ecoportal::API::Common::Client
6
+ class Client < Common::Client
7
+ attr_accessor :logger
8
+
9
+ # @note the `api_key` will be automatically added as parameter `X-ECOPORTAL-API-KEY` in the header of the http requests.
10
+ def initialize(api_key:, version: "v2", host: "live.ecoportal.com", logger: nil)
11
+ super(api_key: api_key, version: "v2", host: host, logger: logger)
12
+ end
13
+
14
+ def delete(path)
15
+ raise "DELETE operation does not have integration for api #{@version}"
16
+ end
17
+
18
+ # @see Ecoportal::API::Common::Client#post
19
+ # @param params [Hash] the header paramters of the http request (not including the api key).
20
+ # @option params [String] :template_id original template.
21
+ def post(path, data:, params: {})
22
+ instrument("POST", path, params) do
23
+ request do |http|
24
+ http.post(url_for(path), json: data, params: params)
25
+ end
26
+ end
27
+ end
28
+
29
+ # Creates a HTTP object adding the `X-ECOPORTAL-API-KEY` param to the header.
30
+ # @note It configures HTTP so it only allows body data in json format.
31
+ # @return [HTTP] HTTP object.
32
+ def base_request
33
+ @base_request ||= HTTP.headers("X-ECOPORTAL-API-KEY" => @api_key).accept(:json)
34
+ end
35
+
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,279 @@
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ module Content
5
+ class CollectionModel < Content::DoubleModel
6
+
7
+ class << self
8
+ attr_writer :klass
9
+ attr_accessor :order_matters, :order_key
10
+
11
+ def items_key
12
+ @items_key ||= "id"
13
+ end
14
+
15
+ def items_key=(value)
16
+ @items_key = value && value.to_s.freeze
17
+ end
18
+
19
+ # Resolves to the nuclear `Class` of the elements
20
+ # @note
21
+ # - use block to define `klass` callback
22
+ # @param value [Hash] base `doc` (raw object) to create the object with
23
+ # @yield [doc] identifies the target `class` of the raw object
24
+ # @yieldparam doc [Hash]
25
+ # @yieldreturn [Klass] the target `class`
26
+ # @return [Klass] the target `class`
27
+ def klass(value = NOT_USED, &block)
28
+ if block
29
+ @klass = block
30
+ block.call(value) if value != NOT_USED
31
+ elsif used_param?(value)
32
+ if @klass.is_a?(Proc)
33
+ @klass.call(value)
34
+ else
35
+ resolve_class(@klass, exception: false)
36
+ end
37
+ else
38
+ resolve_class(@klass, exception: false)
39
+ end
40
+ end
41
+
42
+ # Generates a new object of the target class
43
+ # @note
44
+ # - use block to define `new_item` callback, which will prevail over `klass`
45
+ # - if `new_item` callback was **not** defined, it is required to defnie `klass`
46
+ # @param doc [Hash] doc to parse
47
+ # @note if block is given, it ignores `doc`
48
+ # @yield [doc, parent, key] creates an object instance of the target `klass`
49
+ # @yieldparam doc [Hash]
50
+ # @yieldreturn [Klass] instance object of the target `klass`
51
+ # @return [Klass] instance object of the target `klass`
52
+ def new_item(doc = NOT_USED, parent: nil, key: nil, &block)
53
+ if block
54
+ @new_item = block
55
+ elsif used_param?(doc)
56
+ raise "You should define either a 'klass' or a 'new_item' callback first" unless klass?
57
+ if @new_item
58
+ @new_item.call(doc, parent, key)
59
+ else
60
+ if target_class = self.klass(doc)
61
+ doc.is_a?(target_class) ? doc : target_class.new(doc, parent: parent, key: key)
62
+ else
63
+ raise "Could not find a class for: #{doc}"
64
+ end
65
+ end
66
+ else
67
+ raise "To define the 'new_item' callback (factory), you need to use a block"
68
+ end
69
+ end
70
+
71
+ # @return [Boolean] are there the factory logics to build item objects defined?
72
+ def klass?
73
+ @klass || @new_item
74
+ end
75
+
76
+ def doc_class(name)
77
+ dim_class = new_class(name, inherits: Common::Content::ArrayModel) do |klass|
78
+ klass.order_matters = order_matters
79
+ klass.uniq = uniq
80
+ end
81
+ end
82
+
83
+ end
84
+
85
+ include Enumerable
86
+
87
+ def initialize(ini_doc = [], parent: self, key: nil)
88
+ unless self.class.klass?
89
+ raise "Undefined base 'klass' or 'new_item' callback for #{self.class}"
90
+ end
91
+
92
+ ini_doc = case ini_doc
93
+ when Array
94
+ ini_doc
95
+ when Enumerable
96
+ ini_doc.to_a
97
+ else
98
+ []
99
+ end
100
+
101
+ super(ini_doc, parent: parent, key: key)
102
+ end
103
+
104
+ # @return [Class] the class of the elements of the Collection
105
+ def items_class
106
+ self.class.klass
107
+ end
108
+
109
+ # Transforms `value` into the actual `key` to access the object in the doc `Array`
110
+ def _doc_key(value)
111
+ #print "*(#{value.class})"
112
+ return super(value) unless value.is_a?(Hash) || value.is_a?(Content::DoubleModel)
113
+ if id = get_key(value)
114
+ #print "^"
115
+ _doc_items.index {|item| get_key(item) == id}.tap do |p|
116
+ #print "{{#{p}}}"
117
+ end
118
+ else
119
+ raise UnlinkedModel.new("Can't find child: #{value}")
120
+ end
121
+ end
122
+
123
+ def length; count; end
124
+ def empty?; count == 0; end
125
+ def present?; count > 0; end
126
+
127
+ def each(&block)
128
+ return to_enum(:each) unless block
129
+ _items.each(&block)
130
+ end
131
+
132
+ def _items
133
+ return @_items if @_items
134
+ [].tap do |elements|
135
+ variable_set(:@_items, elements)
136
+ _doc_items.each do |item_doc|
137
+ elements << new_item(item_doc)
138
+ end
139
+ end
140
+ end
141
+
142
+ # Get an element usign the `key`.
143
+ def [](value)
144
+ items_by_key[get_key(value)]
145
+ end
146
+
147
+ def values_at(*keys)
148
+ keys.map {|key| self[key]}
149
+ end
150
+
151
+ # Tries to find the element `value`, if it exists, it updates it
152
+ # Otherwise it pushes it to the end
153
+ # @return the element
154
+ def upsert!(value, pos: NOT_USED, before: NOT_USED, after: NOT_USED)
155
+ unless value.is_a?(Hash) || value.is_a?(Content::DoubleModel)
156
+ raise "'Content::DoubleModel' or 'Hash' doc required"
157
+ end
158
+ item_doc = value.is_a?(Content::DoubleModel)? value.doc : value
159
+ item_doc = JSON.parse(item_doc.to_json)
160
+ if item = self[value]
161
+ item.replace_doc(item_doc)
162
+ else
163
+ _doc_upsert(item_doc, pos: pos, before: before, after: after)
164
+ end
165
+ (item || self[item_doc]).tap do |item|
166
+ yield(item) if block_given?
167
+ end
168
+ end
169
+
170
+ def delete!(value)
171
+ unless value.is_a?(Hash) || value.is_a?(Content::DoubleModel)
172
+ raise "'Content::DoubleModel' or 'Hash' doc required"
173
+ end
174
+ if item = self[value]
175
+ _doc_delete(item.doc)
176
+ end
177
+ end
178
+
179
+ protected
180
+
181
+ def order_matters?; self.class.order_matters; end
182
+ def uniq?; self.class.uniq; end
183
+ def items_key; self.class.items_key; end
184
+
185
+ def on_change
186
+ variables_remove!
187
+ end
188
+
189
+ # Gets the `key` of the object
190
+ def get_key(value)
191
+ case value
192
+ when Content::DoubleModel
193
+ value.key
194
+ when Hash
195
+ value[items_key]
196
+ when String
197
+ value
198
+ end
199
+ end
200
+
201
+ def _doc_items
202
+ replace_doc([]) unless doc.is_a?(Array)
203
+ doc
204
+ end
205
+
206
+ # @note it does not support a change of `id` on an existing item
207
+ def items_by_key
208
+ return @items_by_key if @indexed
209
+ {}.tap do |hash|
210
+ variable_set(:@items_by_key, hash)
211
+ _items.each {|item| hash[item.key] = item}
212
+ @indexed = true
213
+ end
214
+ end
215
+
216
+ private
217
+
218
+ def new_item(value)
219
+ self.class.new_item(value, parent: self)
220
+ end
221
+
222
+ # Helper to remove tracked down instance variables
223
+ def variable_remove!(key)
224
+ if @items_by_key && (k = get_key(key)) && (item = @items_by_key[k])
225
+ _items.delete(item) if _items.include?(item)
226
+ @items_by_key.delete(k)
227
+ else
228
+ super(key)
229
+ end
230
+ end
231
+
232
+ # Removes all the persistent variables
233
+ def variables_remove!
234
+ @indexed = false
235
+ super
236
+ end
237
+
238
+ def _doc_delete(value)
239
+ if current_pos = _doc_key(value)
240
+ _doc_items.delete_at(current_pos)
241
+ on_change
242
+ end
243
+ end
244
+
245
+ def _doc_upsert(value, pos: NOT_USED, before: NOT_USED, after: NOT_USED)
246
+ current_pos = _doc_key(value)
247
+ pos = case
248
+ when used_param?(pos)
249
+ pos
250
+ when used_param?(before)
251
+ _doc_key(before)
252
+ when used_param?(after)
253
+ #puts "to add after #{after.id}"
254
+ if i = _doc_key(after)
255
+ i + 1
256
+ end
257
+ end
258
+
259
+ pos ||= current_pos
260
+
261
+ if current_pos && pos
262
+ _doc_items.delete_at(current_pos)
263
+ pos = (pos <= current_pos)? pos : pos - 1
264
+ end
265
+
266
+ pos = (pos && pos < _doc_items.length)? pos : _doc_items.length
267
+
268
+ pos.tap do |i|
269
+ _doc_items.insert(pos, value)
270
+ on_change
271
+ end
272
+
273
+ end
274
+
275
+ end
276
+ end
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,67 @@
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ module Content
5
+ module DocHelpers
6
+
7
+ # Helper used to build the `body` of an HTTP request
8
+ # @param doc [Page, Hash] hashable object
9
+ # @return [Hash, nil] the `patch` formated `data` ready to include as `body` of a HTTP request
10
+ def get_body(doc, level: "page")
11
+ {}.tap do |body|
12
+ body["#{level}"] = case
13
+ when doc.respond_to?(:as_update)
14
+ doc.as_update
15
+ when doc.respond_to?(:as_json)
16
+ Common::Content::HashPatchDiff.patch_diff(doc.as_json)
17
+ when doc.is_a?(Hash)
18
+ Common::Content::HashPatchDiff.patch_diff(doc)
19
+ else
20
+ raise "Could not get body for doc: #{doc}"
21
+ end
22
+ end
23
+ end
24
+
25
+ # Helper used to **identify** the `id` of the target object
26
+ # @param doc [Page, Hash] hashable object
27
+ # @param exception [Boolean] states if `id` **must** be present
28
+ # @return [Hash, nil] the `patch` formated `data` ready to include as `body` of a HTTP request
29
+ def get_id(doc, exception: true)
30
+ id = nil
31
+ id ||= doc.id if doc.respond_to?(:id)
32
+ id ||= doc["id"] if doc.is_a?(Hash)
33
+ id ||= doc if doc.is_a?(String)
34
+ raise "No ID has been given!" unless id || !exception
35
+ id
36
+ end
37
+
38
+ # Helper used to **identify** the `id` s of objects contained in an `Array`
39
+ # @param doc [Array] the source Array
40
+ # @return [Array<String>] the `id` s thereof
41
+ def array_ids(arr)
42
+ return [] if !arr.is_a?(Array) || arr.empty?
43
+ arr.map {|item| get_id(item, exception: false)}
44
+ end
45
+
46
+ # Helper used to **identify** in an Array the `position` of an object with certain `id`
47
+ # @param doc [Array] the source Array
48
+ # @return [Integer] the `position` thereof
49
+ def array_id_index(arr, id)
50
+ return unless arr.is_a?(Array)
51
+ arr.index {|item| get_id(item, exception: false) == id}
52
+ end
53
+
54
+ # Helper used to **get** in an Array and `object` item with certain `id`
55
+ # @param doc [Array] the source Array
56
+ # @return [Integer] the `object` with that `id`
57
+ def array_id_item(arr, id)
58
+ if idx = array_id_index(arr, id)
59
+ arr[idx]
60
+ end
61
+ end
62
+
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,356 @@
1
+ require 'securerandom'
2
+
3
+ module Ecoportal
4
+ module API
5
+ module Common
6
+ module Content
7
+ # Basic model class, to **build _get_ / _set_ `methods`** for a given property
8
+ # which differs of `attr_*` ruby native class methods because `pass*`
9
+ # completelly **links** the methods **to a subjacent `Hash` model**
10
+ class DoubleModel < Common::BaseModel
11
+ NOT_USED = Common::Content::ClassHelpers::NOT_USED
12
+ extend Common::Content::ClassHelpers
13
+
14
+ class UnlinkedModel < Exception
15
+ def initialize (msg = "Something went wrong when linking the document.", from: nil, key: nil)
16
+ msg += " From: #{from}." if from
17
+ msg += " key: #{key}." if key
18
+ super(msg)
19
+ end
20
+ end
21
+
22
+ class << self
23
+ attr_reader :key
24
+
25
+ def key?
26
+ !!key
27
+ end
28
+
29
+ def key=(value)
30
+ @key = value.to_s.freeze
31
+ end
32
+
33
+ def new_uuid(length: 12)
34
+ SecureRandom.hex(length)
35
+ end
36
+
37
+ # Same as `attr_reader` but links to a subjacent `Hash` model property
38
+ # @note it does **not** create an _instance variable_
39
+ def pass_reader(*methods)
40
+ methods.each do |method|
41
+ method = method.to_s.freeze
42
+
43
+ define_method method do
44
+ value = send(:doc)[method]
45
+ value = yield(value) if block_given?
46
+ value
47
+ end
48
+ end
49
+ self
50
+ end
51
+
52
+ # Same as `attr_writer` but links to a subjacent `Hash` model property
53
+ # @note it does **not** create an _instance variable_
54
+ def pass_writer(*methods)
55
+ methods.each do |method|
56
+ method = method.to_s.freeze
57
+
58
+ define_method "#{method}=" do |value|
59
+ value = yield(value) if block_given?
60
+ send(:doc)[method] = value
61
+ end
62
+ end
63
+ self
64
+ end
65
+
66
+ # This method is essential to give stability to the model
67
+ # @note `Content::CollectionModel` needs to find elements in the doc `Array`.
68
+ # The only way to do it is via the access key (i.e. `id`). However, there is
69
+ # no chance you can avoid invinite loop for `get_key` without setting an
70
+ # instance variable key at the moment of the object creation, when the
71
+ # `doc` is firstly received
72
+ def passkey(method)
73
+ method = method.to_s.freeze
74
+ var = instance_variable_name(method)
75
+ self.key = method
76
+
77
+ define_method method do
78
+ return instance_variable_get(var) if instance_variable_defined?(var)
79
+ value = send(:doc)[method]
80
+ value = yield(value) if block_given?
81
+ value
82
+ end
83
+
84
+ define_method "#{method}=" do |value|
85
+ variable_set(var, value)
86
+ value = yield(value) if block_given?
87
+ send(:doc)[method] = value
88
+ end
89
+
90
+ self
91
+ end
92
+
93
+ # Same as `attr_accessor` but links to a subjacent `Hash` model property
94
+ # @param read_only [Boolean] should it only define the reader?
95
+ def passthrough(*methods, read_only: false)
96
+ pass_reader *methods
97
+ pass_writer *methods unless read_only
98
+ self
99
+ end
100
+
101
+ # To link as a `Time` date to a subjacent `Hash` model property
102
+ # @see Ecoportal::API::Common::Content::DoubleModel#passthrough
103
+ # @param read_only [Boolean] should it only define the reader?
104
+ def passdate(*methods, read_only: false)
105
+ pass_reader(*methods) {|value| to_time(value)}
106
+ unless read_only
107
+ pass_writer(*methods) {|value| to_time(value)&.iso8601}
108
+ end
109
+ self
110
+ end
111
+
112
+ # To link as plain `Array` to a subjacent `Hash` model property
113
+ # @param order_matters [Boolean] does the order matter
114
+ # @param uniq [Boolean] should it contain unique elements
115
+ def passarray(*methods, order_matters: true, uniq: true)
116
+ methods.each do |method|
117
+ method = method.to_s.freeze
118
+ var = instance_variable_name(method)
119
+
120
+ dim_class = new_class(method, inherits: Common::Content::ArrayModel) do |klass|
121
+ klass.order_matters = order_matters
122
+ klass.uniq = uniq
123
+ end
124
+
125
+ define_method method do
126
+ return instance_variable_get(var) if instance_variable_defined?(var)
127
+ new_obj = dim_class.new(parent: self, key: method)
128
+ variable_set(var, new_obj)
129
+ end
130
+ end
131
+ end
132
+
133
+ # Helper to embed one nested object under one property
134
+ def embeds_one(method, key: method, nullable: false, klass:)
135
+ embed(method, key: key, nullable: nullable, multiple: false, klass: klass)
136
+ end
137
+
138
+ # @note
139
+ # - if you have a dedicated `Enumerable` class to manage `many`, you should use `:enum_class`
140
+ # - otherwise, just indicate the child class in `:klass` and it will auto generate the class
141
+ # @param
142
+ def embeds_many(method, key: method, order_matters: false, order_key: nil, klass: nil, enum_class: nil)
143
+ if enum_class
144
+ eclass = enum_class
145
+ elsif klass
146
+ eclass = new_class(method, inherits: Common::Content::CollectionModel) do |dim_class|
147
+ dim_class.klass = klass
148
+ dim_class.order_matters = order_matters
149
+ dim_class.order_key = order_key
150
+ end
151
+ else
152
+ raise "You should either specify the 'klass' of the elements or the 'enum_class'"
153
+ end
154
+ embed(method, key: key, multiple: true, klass: eclass)
155
+ end
156
+
157
+ private
158
+
159
+ def embed(method, key: method, nullable: false, multiple: false, klass:)
160
+ method = method.to_s.freeze
161
+ var = instance_variable_name(method).freeze
162
+ k = key.to_s.freeze
163
+
164
+ # retrieving method (getter)
165
+ define_method(method) do
166
+ return instance_variable_get(var) if instance_variable_defined?(var)
167
+ unless nullable
168
+ doc[k] ||= multiple ? [] : {}
169
+ end
170
+ return variable_set(var, nil) unless doc[k]
171
+
172
+ self.class.resolve_class(klass).new(
173
+ doc[k], parent: self, key: k
174
+ ).tap {|obj| variable_set(var, obj)}
175
+ end
176
+ end
177
+
178
+ end
179
+
180
+ attr_reader :_parent, :_key
181
+
182
+ def initialize(doc = {}, parent: self, key: nil)
183
+ @_dim_vars = []
184
+ @_parent = parent || self
185
+ @_key = key || self
186
+
187
+ if _parent == self
188
+ @doc = doc
189
+ @original_doc = JSON.parse(@doc.to_json)
190
+ end
191
+
192
+ if key_method? && doc && doc.is_a?(Hash)
193
+ self.key = doc[key_method]
194
+ #puts "\n$(#{self.key}<=>#{self.class})"
195
+ end
196
+ end
197
+
198
+ def root
199
+ return self if is_root?
200
+ _parent.root
201
+ end
202
+
203
+ def key
204
+ raise "No key_method defined for #{self.class}" unless key_method?
205
+ self.method(key_method).call
206
+ end
207
+
208
+ def key=(value)
209
+ raise "No key_method defined for #{self.class}" unless key_method?
210
+ method = "#{key_method}="
211
+ self.method(method).call(value)
212
+ end
213
+
214
+ # Offers a method for child classes to transform the key,
215
+ # provided that the child's `doc` can be accessed
216
+ def _doc_key(value)
217
+ if value.is_a?(Content::DoubleModel) && !value.is_root?
218
+ #print "?(#{value.class}<=#{value._parent.class})"
219
+ value._parent._doc_key(value)
220
+ else
221
+ #print "!(#{value}<=#{self.class})"
222
+ value
223
+ end
224
+ end
225
+
226
+ def doc
227
+ raise UnlinkedModel.new(from: "#{self.class}#doc", key: _key) unless linked?
228
+ return @doc if is_root?
229
+ _parent.doc.dig(*[_doc_key(_key)].flatten)
230
+ end
231
+
232
+ def original_doc
233
+ raise UnlinkedModel.new(from: "#{self.class}#original_doc", key: _key) unless linked?
234
+ return @original_doc if is_root?
235
+ _parent.original_doc.dig(*[_doc_key(_key)].flatten)
236
+ end
237
+
238
+ def as_json
239
+ doc
240
+ end
241
+
242
+ def to_json(*args)
243
+ doc.to_json(*args)
244
+ end
245
+
246
+ def as_update
247
+ new_doc = as_json
248
+ Common::Content::HashDiffPatch.patch_diff(new_doc, original_doc)
249
+ end
250
+
251
+ def dirty?
252
+ as_update != {}
253
+ end
254
+
255
+ def consolidate!
256
+ replace_original_doc(JSON.parse(doc.to_json))
257
+ end
258
+
259
+ def reset!(key = nil)
260
+ if key
261
+ keys = [].push(key).compact
262
+ odoc = original_doc.dig(*keys)
263
+ dig_set(doc, key, odoc && JSON.parse(odoc.to_json))
264
+
265
+ else
266
+ replace_doc(JSON.parse(original_doc.to_json))
267
+ end
268
+ end
269
+
270
+ def print_pretty
271
+ puts JSON.pretty_generate(as_json)
272
+ self
273
+ end
274
+
275
+ def replace_doc(new_doc)
276
+ raise UnlinkedModel.new(from: "#{self.class}#replace_doc", key: _key) unless linked?
277
+ if is_root?
278
+ @doc = new_doc
279
+ else
280
+ dig_set(_parent.doc, [_doc_key(_key)].flatten, new_doc)
281
+ _parent.variable_remove!(_key)
282
+ variables_remove!
283
+ end
284
+ end
285
+
286
+ protected
287
+
288
+ def is_root?
289
+ _parent == self && !!defined?(@doc)
290
+ end
291
+
292
+ def linked?
293
+ is_root? || !!_parent.doc
294
+ end
295
+
296
+ def replace_original_doc(new_doc)
297
+ raise UnlinkedModel.new(from: "#{self.class}#replace_original_doc", key: _key) unless linked?
298
+ if is_root?
299
+ @orginal_doc = new_doc
300
+ else
301
+ dig_set(_parent.orginal_doc, [_doc_key(_key)].flatten, new_doc)
302
+ end
303
+ end
304
+
305
+ # Helper to track down persistent variables
306
+ def variable_set(key, value)
307
+ var = instance_variable_name(key)
308
+ @_dim_vars.push(var).uniq!
309
+ instance_variable_set(var, value)
310
+ end
311
+
312
+ # Helper to remove tracked down instance variables
313
+ def variable_remove!(key)
314
+ var = instance_variable_name(key)
315
+ unless !@_dim_vars.include?(var)
316
+ @_dim_vars.delete(var)
317
+ remove_instance_variable(var)
318
+ end
319
+ end
320
+
321
+ # Removes all the persistent variables
322
+ def variables_remove!
323
+ @_dim_vars.dup.map {|k| variable_remove!(k)}
324
+ end
325
+
326
+ private
327
+
328
+ def instance_variable_name(key)
329
+ self.class.instance_variable_name(key)
330
+ end
331
+
332
+ def dig_set(obj, keys, value)
333
+ if keys.length == 1
334
+ obj[keys.first] = value
335
+ else
336
+ dig_set(obj[keys.first], keys.slice(1..-1), value)
337
+ end
338
+ end
339
+
340
+ def used_param?(val)
341
+ self.class.used_param?(val)
342
+ end
343
+
344
+ def key_method?
345
+ self.class.key?
346
+ end
347
+
348
+ def key_method
349
+ self.class.key
350
+ end
351
+
352
+ end
353
+ end
354
+ end
355
+ end
356
+ end