ecoportal-api-v2 0.8.4

Sign up to get free protection for your applications and to get access to all the features.
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