ecoportal-api-oozes 0.5.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.
Files changed (60) 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/Gemfile +6 -0
  8. data/LICENSE +21 -0
  9. data/README.md +20 -0
  10. data/Rakefile +27 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/ecoportal-api-oozes.gemspec +31 -0
  14. data/lib/ecoportal/api-oozes.rb +10 -0
  15. data/lib/ecoportal/api/common.rb +18 -0
  16. data/lib/ecoportal/api/common/content.rb +18 -0
  17. data/lib/ecoportal/api/common/content/array_model.rb +287 -0
  18. data/lib/ecoportal/api/common/content/class_helpers.rb +111 -0
  19. data/lib/ecoportal/api/common/content/client.rb +40 -0
  20. data/lib/ecoportal/api/common/content/collection_model.rb +223 -0
  21. data/lib/ecoportal/api/common/content/doc_helpers.rb +67 -0
  22. data/lib/ecoportal/api/common/content/double_model.rb +334 -0
  23. data/lib/ecoportal/api/common/content/hash_diff_patch.rb +162 -0
  24. data/lib/ecoportal/api/common/content/string_digest.rb +22 -0
  25. data/lib/ecoportal/api/common/content/wrapped_response.rb +42 -0
  26. data/lib/ecoportal/api/v2.rb +48 -0
  27. data/lib/ecoportal/api/v2/page.rb +30 -0
  28. data/lib/ecoportal/api/v2/page/component.rb +105 -0
  29. data/lib/ecoportal/api/v2/page/component/action.rb +17 -0
  30. data/lib/ecoportal/api/v2/page/component/action_field.rb +16 -0
  31. data/lib/ecoportal/api/v2/page/component/checklist_field.rb +16 -0
  32. data/lib/ecoportal/api/v2/page/component/checklist_item.rb +15 -0
  33. data/lib/ecoportal/api/v2/page/component/date_field.rb +13 -0
  34. data/lib/ecoportal/api/v2/page/component/file.rb +16 -0
  35. data/lib/ecoportal/api/v2/page/component/files_field.rb +14 -0
  36. data/lib/ecoportal/api/v2/page/component/gauge_field.rb +13 -0
  37. data/lib/ecoportal/api/v2/page/component/geo_field.rb +13 -0
  38. data/lib/ecoportal/api/v2/page/component/image.rb +16 -0
  39. data/lib/ecoportal/api/v2/page/component/images_field.rb +14 -0
  40. data/lib/ecoportal/api/v2/page/component/law_field.rb +12 -0
  41. data/lib/ecoportal/api/v2/page/component/number_field.rb +12 -0
  42. data/lib/ecoportal/api/v2/page/component/people_field.rb +13 -0
  43. data/lib/ecoportal/api/v2/page/component/plain_text_field.rb +13 -0
  44. data/lib/ecoportal/api/v2/page/component/reference_field.rb +12 -0
  45. data/lib/ecoportal/api/v2/page/component/rich_text_field.rb +13 -0
  46. data/lib/ecoportal/api/v2/page/component/selection_field.rb +18 -0
  47. data/lib/ecoportal/api/v2/page/component/selection_option.rb +15 -0
  48. data/lib/ecoportal/api/v2/page/component/signature_field.rb +14 -0
  49. data/lib/ecoportal/api/v2/page/component/tag_field.rb +12 -0
  50. data/lib/ecoportal/api/v2/page/components.rb +20 -0
  51. data/lib/ecoportal/api/v2/page/section.rb +36 -0
  52. data/lib/ecoportal/api/v2/page/sections.rb +14 -0
  53. data/lib/ecoportal/api/v2/page/stage.rb +16 -0
  54. data/lib/ecoportal/api/v2/page/stages.rb +14 -0
  55. data/lib/ecoportal/api/v2/pages.rb +63 -0
  56. data/lib/ecoportal/api/v2/register.rb +36 -0
  57. data/lib/ecoportal/api/v2/registers.rb +35 -0
  58. data/lib/ecoportal/api/v2/template.rb +10 -0
  59. data/lib/ecoportal/api/v2/version.rb +7 -0
  60. metadata +219 -0
@@ -0,0 +1,111 @@
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ module Content
5
+ module ClassHelpers
6
+ include Common::BaseClass
7
+ NOT_USED = "no_used!"
8
+
9
+ # Class resolver
10
+ # @note it caches the resolved `klass`es
11
+ # @raise [Exception] when could not resolve if `exception` is `true`
12
+ # @param klass [Class, String, Symbol] the class to resolve
13
+ # @param exception [Boolean] if it should raise exception when could not resolve
14
+ # @return [Class] the `Class` constant
15
+ def resolve_class(klass, exception: true)
16
+ @resolved ||= {}
17
+ @resolved[klass] ||=
18
+ case klass
19
+ when Class
20
+ klass
21
+ when String
22
+ begin
23
+ Kernel.const_get(klass)
24
+ rescue NameError => e
25
+ raise if exception
26
+ end
27
+ when Symbol
28
+ resolve_class(self.send(klass))
29
+ else
30
+ raise "Unknown class: #{klass}" if exception
31
+ end
32
+ end
33
+
34
+ # Helper to normalize `key` into a correct `ruby` **constant name**
35
+ # @param key [String, Symbol] to be normalized
36
+ # @return [String] a correct constant name
37
+ def to_constant(key)
38
+ str_name = key.to_s.strip.split(/[\-\_ ]/i).compact.map do |str|
39
+ str.slice(0).upcase + str.slice(1..-1).downcase
40
+ end.join("")
41
+ end
42
+
43
+ # Helper to create an instance variable `name`
44
+ # @param [String, Symbol] the name of the variable
45
+ # @reutrn [String] the name of the created instance variable
46
+ def instance_variable_name(name)
47
+ str = name.to_s
48
+ str = "@#{str}" unless str.start_with?("@")
49
+ str
50
+ end
51
+
52
+ # If the class for `name` exists, it returns it. Otherwise it generates it.
53
+ # @param name [String, Symbol] the name of the new class
54
+ # @param inherits [Class] the parent class to _inherit_ from
55
+ # @yield [child_class] configure the new class
56
+ # @yieldparam child_class [Class] the new class
57
+ # @return [Class] the new generated class
58
+ def new_class(name, inherits:)
59
+ name = name.to_sym.freeze
60
+ class_name = to_constant(name)
61
+ full_class_name = "#{inherits}::#{class_name}"
62
+
63
+ unless target_class = resolve_class(full_class_name, exception: false)
64
+ target_class = Class.new(inherits)
65
+ self.const_set class_name, target_class
66
+ end
67
+
68
+ target_class.tap do |klass|
69
+ yield(klass) if block_given?
70
+ end
71
+ end
72
+
73
+ # Helper to parse a value into a `Time` object.
74
+ # @raise [Exception] if `exception` is `true` and could not convert
75
+ # @param value [String, Date] the value to convert to `Time`
76
+ # @param exception [Boolean] if should raise `Exception` when could not convert
77
+ # @return
78
+ def to_time(value, exception: true)
79
+ case value
80
+ when NilClass
81
+ value
82
+ when String
83
+ begin
84
+ Time.parse(value)
85
+ rescue ArgumentArgument => e
86
+ raise if exception
87
+ nil
88
+ end
89
+ when Date
90
+ Time.parse(value.to_s)
91
+ when Time
92
+ value
93
+ else
94
+ to_time(value.to_s) if value.respond_to?(:to_s)
95
+ end
96
+ end
97
+
98
+ # Helper to determine if a paramter has been used
99
+ # @note to effectivelly use this helper, you should initialize your target
100
+ # paramters with the constant `NOT_USED`
101
+ # @param val [] the value of the paramter
102
+ # @return [Boolean] `true` if value other than `NOT_USED`, `false` otherwise
103
+ def used_param?(val)
104
+ val != NOT_USED
105
+ end
106
+
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -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,223 @@
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
+ # Transforms `value` into the actual `key` to access the object in the doc `Array`
105
+ def _doc_key(value)
106
+ #print "*(#{value.class})"
107
+ return super(value) unless value.is_a?(Hash) || value.is_a?(Content::DoubleModel)
108
+ if id = get_key(value)
109
+ #print "^"
110
+ _doc_items.index {|item| get_key(item) == id}.tap do |p|
111
+ #print "{{#{p}}}"
112
+ end
113
+ else
114
+ raise UnlinkedModel.new("Can't find child: #{value}")
115
+ end
116
+ end
117
+
118
+ def length; count; end
119
+ def empty?; count == 0; end
120
+ def present?; count > 0; end
121
+
122
+ def each(&block)
123
+ return to_enum(:each) unless block
124
+ _items.each(&block)
125
+ end
126
+
127
+ def _items
128
+ return @_items if @_items
129
+ [].tap do |elements|
130
+ variable_set(:@_items, elements)
131
+ _doc_items.each do |item_doc|
132
+ elements << new_item(item_doc)
133
+ end
134
+ end
135
+ end
136
+
137
+ def [](value)
138
+ items_by_key[get_key(value)]
139
+ end
140
+
141
+ def values_at(*keys)
142
+ keys.map {|key| self[key]}
143
+ end
144
+
145
+ # Tries to find the element `value`, if it exists, it updates it
146
+ # Otherwise it pushes it to the end
147
+ # @return the element
148
+ def upsert!(value)
149
+ unless value.is_a?(Hash) || value.is_a?(Content::DoubleModel)
150
+ raise "'Content::DoubleModel' or 'Hash' doc required"
151
+ end
152
+ item_doc = value.is_a?(Content::DoubleModel)? value.doc : value
153
+ if item = self[value]
154
+ item.replace_doc(JSON.parse(item_doc.to_json))
155
+ else
156
+ item = new_item(item_doc)
157
+ _doc_items << item.doc
158
+ end
159
+ item
160
+ end
161
+
162
+ protected
163
+
164
+ def order_matters?; self.class.order_matters; end
165
+ def uniq?; self.class.uniq; end
166
+ def items_key; self.class.items_key; end
167
+
168
+ # Gets the `key` of the object
169
+ def get_key(value)
170
+ case value
171
+ when Content::DoubleModel
172
+ value.key
173
+ when Hash
174
+ value[items_key]
175
+ when String
176
+ value
177
+ end
178
+ end
179
+
180
+ def _doc_items
181
+ replace_doc([]) unless doc.is_a?(Array)
182
+ doc
183
+ end
184
+
185
+ # @note it does not support a change of `id` on an existing item
186
+ def items_by_key
187
+ return @items_by_key if @indexed
188
+ {}.tap do |hash|
189
+ variable_set(:@items_by_key, hash)
190
+ _items.each {|item| hash[item.key] = item}
191
+ @indexed = true
192
+ end
193
+ end
194
+
195
+ private
196
+
197
+ def new_item(value)
198
+ self.class.new_item(value, parent: self)
199
+ end
200
+
201
+ private
202
+
203
+ # Helper to remove tracked down instance variables
204
+ def variable_remove!(key)
205
+ if (k = get_key(key)) && (item = @items_by_key[k])
206
+ _items.delete(item) if _items.include?(item)
207
+ @items_by_key.delete(k)
208
+ else
209
+ super(key)
210
+ end
211
+ end
212
+
213
+ # Removes all the persistent variables
214
+ def variables_remove!
215
+ @indexed = false
216
+ super
217
+ end
218
+
219
+ end
220
+ end
221
+ end
222
+ end
223
+ 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,334 @@
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ module Content
5
+ # Basic model class, to **build _get_ / _set_ `methods`** for a given property
6
+ # which differs of `attr_*` ruby native class methods because `pass*`
7
+ # completelly **links** the methods **to a subjacent `Hash` model**
8
+ class DoubleModel < Common::BaseModel
9
+ NOT_USED = Common::Content::ClassHelpers::NOT_USED
10
+ extend Common::Content::ClassHelpers
11
+
12
+ class UnlinkedModel < Exception
13
+ def initialize (msg = "Something went wrong when linking the document.", from: nil, key: nil)
14
+ msg += " From: #{from}." if from
15
+ msg += " key: #{key}." if key
16
+ super(msg)
17
+ end
18
+ end
19
+
20
+ class << self
21
+ attr_reader :key
22
+
23
+ def key?
24
+ !!key
25
+ end
26
+
27
+ def key=(value)
28
+ @key = value.to_s.freeze
29
+ end
30
+
31
+ # Same as `attr_reader` but links to a subjacent `Hash` model property
32
+ # @note it does **not** create an _instance variable_
33
+ def pass_reader(*methods)
34
+ methods.each do |method|
35
+ method = method.to_s.freeze
36
+
37
+ define_method method do
38
+ value = send(:doc)[method]
39
+ value = yield(value) if block_given?
40
+ value
41
+ end
42
+ end
43
+ self
44
+ end
45
+
46
+ # Same as `attr_writer` but links to a subjacent `Hash` model property
47
+ # @note it does **not** create an _instance variable_
48
+ def pass_writer(*methods)
49
+ methods.each do |method|
50
+ method = method.to_s.freeze
51
+
52
+ define_method "#{method}=" do |value|
53
+ value = yield(value) if block_given?
54
+ send(:doc)[method] = value
55
+ end
56
+ end
57
+ self
58
+ end
59
+
60
+ # This method is essential to give stability to the model
61
+ # @note `Content::CollectionModel` needs to find elements in the doc `Array`.
62
+ # The only way to do it is via the access key (i.e. `id`). However, there is
63
+ # no chance you can avoid invinite loop for `get_key` without setting an
64
+ # instance variable key at the moment of the object creation, when the
65
+ # `doc` is firstly received
66
+ def passkey(method)
67
+ method = method.to_s.freeze
68
+ var = instance_variable_name(method)
69
+ self.key = method
70
+
71
+ define_method method do
72
+ return instance_variable_get(var) if instance_variable_defined?(var)
73
+ value = send(:doc)[method]
74
+ value = yield(value) if block_given?
75
+ value
76
+ end
77
+
78
+ define_method "#{method}=" do |value|
79
+ variable_set(var, value)
80
+ value = yield(value) if block_given?
81
+ send(:doc)[method] = value
82
+ end
83
+
84
+ self
85
+ end
86
+
87
+ # Same as `attr_accessor` but links to a subjacent `Hash` model property
88
+ # @param read_only [Boolean] should it only define the reader?
89
+ def passthrough(*methods, read_only: false)
90
+ pass_reader *methods
91
+ pass_writer *methods unless read_only
92
+ self
93
+ end
94
+
95
+ # To link as a `Time` date to a subjacent `Hash` model property
96
+ # @see Ecoportal::API::Common::Content::DoubleModel#passthrough
97
+ # @param read_only [Boolean] should it only define the reader?
98
+ def passdate(*methods, read_only: false)
99
+ pass_reader(*methods) {|value| to_time(value)}
100
+ unless read_only
101
+ pass_writer(*methods) {|value| to_time(value)&.iso8601}
102
+ end
103
+ self
104
+ end
105
+
106
+ # To link as plain `Array` to a subjacent `Hash` model property
107
+ # @param order_matters [Boolean] does the order matter
108
+ # @param uniq [Boolean] should it contain unique elements
109
+ def passarray(*methods, order_matters: true, uniq: true)
110
+ methods.each do |method|
111
+ method = method.to_s.freeze
112
+ var = instance_variable_name(method)
113
+
114
+ dim_class = new_class(method, inherits: Common::Content::ArrayModel) do |klass|
115
+ klass.order_matters = order_matters
116
+ klass.uniq = uniq
117
+ end
118
+
119
+ define_method method do
120
+ return instance_variable_get(var) if instance_variable_defined?(var)
121
+ new_obj = dim_class.new(parent: self, key: method)
122
+ variable_set(var, new_obj)
123
+ end
124
+ end
125
+ end
126
+
127
+ # Helper to embed one nested object under one property
128
+ def embeds_one(method, key: method, nullable: false, multiple: false, klass:)
129
+ method = method.to_s.freeze
130
+ var = instance_variable_name(method).freeze
131
+ k = key.to_s.freeze
132
+
133
+ # retrieving method (getter)
134
+ define_method(method) do
135
+ return instance_variable_get(var) if instance_variable_defined?(var)
136
+ unless nullable
137
+ doc[k] ||= multiple ? [] : {}
138
+ end
139
+ return variable_set(var, nil) unless doc[k]
140
+
141
+ self.class.resolve_class(klass).new(
142
+ doc[k], parent: self, key: k
143
+ ).tap {|obj| variable_set(var, obj)}
144
+ end
145
+ end
146
+
147
+ def embeds_multiple(method, key: method, order_matters: false, order_key: nil, klass:)
148
+ dim_class = new_class(method, inherits: Common::Content::CollectionModel) do |dklass|
149
+ dklass.klass = klass
150
+ dklass.order_matters = order_matters
151
+ dklass.order_key = order_key
152
+ end
153
+
154
+ embeds_one(method, key: key, multiple: true, klass: dim_class)
155
+ end
156
+ end
157
+
158
+ attr_reader :_parent, :_key
159
+
160
+ def initialize(doc = {}, parent: self, key: nil)
161
+ @_dim_vars = []
162
+ @_parent = parent || self
163
+ @_key = key || self
164
+
165
+ if _parent == self
166
+ @doc = doc
167
+ @original_doc = JSON.parse(@doc.to_json)
168
+ end
169
+
170
+ if key_method? && doc && doc.is_a?(Hash)
171
+ self.key = doc[key_method]
172
+ #puts "\n$(#{self.key}<=>#{self.class})"
173
+ end
174
+ end
175
+
176
+ def root
177
+ return self if is_root?
178
+ _parent.root
179
+ end
180
+
181
+ def key
182
+ raise "No key_method defined for #{self.class}" unless key_method?
183
+ self.method(key_method).call
184
+ end
185
+
186
+ def key=(value)
187
+ raise "No key_method defined for #{self.class}" unless key_method?
188
+ method = "#{key_method}="
189
+ self.method(method).call(value)
190
+ end
191
+
192
+ # Offers a method for child classes to transform the key,
193
+ # provided that the child's `doc` can be accessed
194
+ def _doc_key(value)
195
+ if value.is_a?(Content::DoubleModel) && !value.is_root?
196
+ #print "?(#{value.class}<=#{value._parent.class})"
197
+ value._parent._doc_key(value)
198
+ else
199
+ #print "!(#{value}<=#{self.class})"
200
+ value
201
+ end
202
+ end
203
+
204
+ def doc
205
+ raise UnlinkedModel.new(from: "#{self.class}#doc", key: _key) unless linked?
206
+ return @doc if is_root?
207
+ _parent.doc.dig(*[_doc_key(_key)].flatten)
208
+ end
209
+
210
+ def original_doc
211
+ raise UnlinkedModel.new(from: "#{self.class}#original_doc", key: _key) unless linked?
212
+ return @original_doc if is_root?
213
+ _parent.original_doc.dig(*[_doc_key(_key)].flatten)
214
+ end
215
+
216
+ def as_json
217
+ doc
218
+ end
219
+
220
+ def to_json(*args)
221
+ doc.to_json(*args)
222
+ end
223
+
224
+ def as_update
225
+ new_doc = as_json
226
+ Common::Content::HashDiffPatch.patch_diff(new_doc, original_doc)
227
+ end
228
+
229
+ def dirty?
230
+ as_update != {}
231
+ end
232
+
233
+ def consolidate!
234
+ replace_original_doc(JSON.parse(doc.to_json))
235
+ end
236
+
237
+ def reset!(key = nil)
238
+ if key
239
+ keys = [].push(key).compact
240
+ odoc = original_doc.dig(*keys)
241
+ dig_set(doc, key, odoc && JSON.parse(odoc.to_json))
242
+
243
+ else
244
+ replace_doc(JSON.parse(original_doc.to_json))
245
+ end
246
+ end
247
+
248
+ #def print
249
+ # puts JSON.pretty_generate(as_json)
250
+ # self
251
+ #end
252
+
253
+ protected
254
+
255
+ def is_root?
256
+ _parent == self && !!defined?(@doc)
257
+ end
258
+
259
+ def linked?
260
+ is_root? || !!_parent.doc
261
+ end
262
+
263
+ def replace_doc(new_doc)
264
+ raise UnlinkedModel.new(from: "#{self.class}#replace_doc", key: _key) unless linked?
265
+ if is_root?
266
+ @doc = new_doc
267
+ else
268
+ dig_set(_parent.doc, [_doc_key(_key)].flatten, new_doc)
269
+ _parent.variable_remove!(_key)
270
+ variables_remove!
271
+ end
272
+ end
273
+
274
+ def replace_original_doc(new_doc)
275
+ raise UnlinkedModel.new(from: "#{self.class}#replace_original_doc", key: _key) unless linked?
276
+ if is_root?
277
+ @orginal_doc = new_doc
278
+ else
279
+ dig_set(_parent.orginal_doc, [_doc_key(_key)].flatten, new_doc)
280
+ end
281
+ end
282
+
283
+ # Helper to track down persistent variables
284
+ def variable_set(key, value)
285
+ var = instance_variable_name(key)
286
+ @_dim_vars.push(var).uniq!
287
+ instance_variable_set(var, value)
288
+ end
289
+
290
+ # Helper to remove tracked down instance variables
291
+ def variable_remove!(key)
292
+ var = instance_variable_name(key)
293
+ unless !@_dim_vars.include?(var)
294
+ @_dim_vars.delete(var)
295
+ remove_instance_variable(var)
296
+ end
297
+ end
298
+
299
+ # Removes all the persistent variables
300
+ def variables_remove!
301
+ @_dim_vars.map {|k| variable_remove!(k)}
302
+ end
303
+
304
+ private
305
+
306
+ def instance_variable_name(key)
307
+ self.class.instance_variable_name(key)
308
+ end
309
+
310
+ def dig_set(obj, keys, value)
311
+ if keys.length == 1
312
+ obj[keys.first] = value
313
+ else
314
+ dig_set(obj[keys.first], keys.slice(1..-1), value)
315
+ end
316
+ end
317
+
318
+ def used_param?(val)
319
+ self.class.used_param?(val)
320
+ end
321
+
322
+ def key_method?
323
+ self.class.key?
324
+ end
325
+
326
+ def key_method
327
+ self.class.key
328
+ end
329
+
330
+ end
331
+ end
332
+ end
333
+ end
334
+ end