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.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +3 -0
- data/.rubocop.yml +55 -0
- data/.travis.yml +5 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +171 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.md +22 -0
- data/Rakefile +27 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/ecoportal-api-v2.gemspec +34 -0
- data/lib/ecoportal/api-v2.rb +10 -0
- data/lib/ecoportal/api/common.rb +18 -0
- data/lib/ecoportal/api/common/content.rb +18 -0
- data/lib/ecoportal/api/common/content/array_model.rb +286 -0
- data/lib/ecoportal/api/common/content/class_helpers.rb +146 -0
- data/lib/ecoportal/api/common/content/client.rb +40 -0
- data/lib/ecoportal/api/common/content/collection_model.rb +279 -0
- data/lib/ecoportal/api/common/content/doc_helpers.rb +67 -0
- data/lib/ecoportal/api/common/content/double_model.rb +356 -0
- data/lib/ecoportal/api/common/content/hash_diff_patch.rb +183 -0
- data/lib/ecoportal/api/common/content/string_digest.rb +27 -0
- data/lib/ecoportal/api/common/content/wrapped_response.rb +42 -0
- data/lib/ecoportal/api/v2.rb +82 -0
- data/lib/ecoportal/api/v2/page.rb +42 -0
- data/lib/ecoportal/api/v2/page/component.rb +133 -0
- data/lib/ecoportal/api/v2/page/component/action.rb +28 -0
- data/lib/ecoportal/api/v2/page/component/action_field.rb +54 -0
- data/lib/ecoportal/api/v2/page/component/chart_field.rb +54 -0
- data/lib/ecoportal/api/v2/page/component/chart_field/frequency.rb +29 -0
- data/lib/ecoportal/api/v2/page/component/chart_field/heatmap.rb +27 -0
- data/lib/ecoportal/api/v2/page/component/chart_field/indicator.rb +26 -0
- data/lib/ecoportal/api/v2/page/component/chart_field/multiseries.rb +31 -0
- data/lib/ecoportal/api/v2/page/component/chart_field/sankey.rb +27 -0
- data/lib/ecoportal/api/v2/page/component/chart_field/serie.rb +26 -0
- data/lib/ecoportal/api/v2/page/component/chart_field/series_config.rb +23 -0
- data/lib/ecoportal/api/v2/page/component/chart_fr_field.rb +32 -0
- data/lib/ecoportal/api/v2/page/component/checklist_field.rb +49 -0
- data/lib/ecoportal/api/v2/page/component/checklist_item.rb +25 -0
- data/lib/ecoportal/api/v2/page/component/date_field.rb +34 -0
- data/lib/ecoportal/api/v2/page/component/file.rb +16 -0
- data/lib/ecoportal/api/v2/page/component/files_field.rb +13 -0
- data/lib/ecoportal/api/v2/page/component/gauge_field.rb +36 -0
- data/lib/ecoportal/api/v2/page/component/gauge_stop.rb +88 -0
- data/lib/ecoportal/api/v2/page/component/geo_field.rb +13 -0
- data/lib/ecoportal/api/v2/page/component/image.rb +16 -0
- data/lib/ecoportal/api/v2/page/component/images_field.rb +23 -0
- data/lib/ecoportal/api/v2/page/component/law_field.rb +12 -0
- data/lib/ecoportal/api/v2/page/component/number_field.rb +12 -0
- data/lib/ecoportal/api/v2/page/component/people_field.rb +25 -0
- data/lib/ecoportal/api/v2/page/component/plain_text_field.rb +15 -0
- data/lib/ecoportal/api/v2/page/component/reference_field.rb +16 -0
- data/lib/ecoportal/api/v2/page/component/rich_text_field.rb +13 -0
- data/lib/ecoportal/api/v2/page/component/selection_field.rb +78 -0
- data/lib/ecoportal/api/v2/page/component/selection_option.rb +25 -0
- data/lib/ecoportal/api/v2/page/component/signature_field.rb +25 -0
- data/lib/ecoportal/api/v2/page/component/tag_field.rb +14 -0
- data/lib/ecoportal/api/v2/page/components.rb +42 -0
- data/lib/ecoportal/api/v2/page/section.rb +59 -0
- data/lib/ecoportal/api/v2/page/sections.rb +47 -0
- data/lib/ecoportal/api/v2/page/stage.rb +29 -0
- data/lib/ecoportal/api/v2/page/stages.rb +26 -0
- data/lib/ecoportal/api/v2/pages.rb +92 -0
- data/lib/ecoportal/api/v2/pages/page_stage.rb +16 -0
- data/lib/ecoportal/api/v2/pages/stages.rb +55 -0
- data/lib/ecoportal/api/v2/people.rb +31 -0
- data/lib/ecoportal/api/v2/registers.rb +89 -0
- data/lib/ecoportal/api/v2/registers/page_result.rb +21 -0
- data/lib/ecoportal/api/v2/registers/register.rb +37 -0
- data/lib/ecoportal/api/v2/registers/stage_result.rb +14 -0
- data/lib/ecoportal/api/v2/registers/stages_result.rb +13 -0
- data/lib/ecoportal/api/v2/registers/template.rb +12 -0
- data/lib/ecoportal/api/v2/version.rb +7 -0
- 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
|