ecoportal-api-oozes 0.5.5

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