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.
- 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/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.md +20 -0
- data/Rakefile +27 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/ecoportal-api-oozes.gemspec +31 -0
- data/lib/ecoportal/api-oozes.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 +287 -0
- data/lib/ecoportal/api/common/content/class_helpers.rb +111 -0
- data/lib/ecoportal/api/common/content/client.rb +40 -0
- data/lib/ecoportal/api/common/content/collection_model.rb +223 -0
- data/lib/ecoportal/api/common/content/doc_helpers.rb +67 -0
- data/lib/ecoportal/api/common/content/double_model.rb +334 -0
- data/lib/ecoportal/api/common/content/hash_diff_patch.rb +162 -0
- data/lib/ecoportal/api/common/content/string_digest.rb +22 -0
- data/lib/ecoportal/api/common/content/wrapped_response.rb +42 -0
- data/lib/ecoportal/api/v2.rb +48 -0
- data/lib/ecoportal/api/v2/page.rb +30 -0
- data/lib/ecoportal/api/v2/page/component.rb +105 -0
- data/lib/ecoportal/api/v2/page/component/action.rb +17 -0
- data/lib/ecoportal/api/v2/page/component/action_field.rb +16 -0
- data/lib/ecoportal/api/v2/page/component/checklist_field.rb +16 -0
- data/lib/ecoportal/api/v2/page/component/checklist_item.rb +15 -0
- data/lib/ecoportal/api/v2/page/component/date_field.rb +13 -0
- data/lib/ecoportal/api/v2/page/component/file.rb +16 -0
- data/lib/ecoportal/api/v2/page/component/files_field.rb +14 -0
- data/lib/ecoportal/api/v2/page/component/gauge_field.rb +13 -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 +14 -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 +13 -0
- data/lib/ecoportal/api/v2/page/component/plain_text_field.rb +13 -0
- data/lib/ecoportal/api/v2/page/component/reference_field.rb +12 -0
- data/lib/ecoportal/api/v2/page/component/rich_text_field.rb +13 -0
- data/lib/ecoportal/api/v2/page/component/selection_field.rb +18 -0
- data/lib/ecoportal/api/v2/page/component/selection_option.rb +15 -0
- data/lib/ecoportal/api/v2/page/component/signature_field.rb +14 -0
- data/lib/ecoportal/api/v2/page/component/tag_field.rb +12 -0
- data/lib/ecoportal/api/v2/page/components.rb +20 -0
- data/lib/ecoportal/api/v2/page/section.rb +36 -0
- data/lib/ecoportal/api/v2/page/sections.rb +14 -0
- data/lib/ecoportal/api/v2/page/stage.rb +16 -0
- data/lib/ecoportal/api/v2/page/stages.rb +14 -0
- data/lib/ecoportal/api/v2/pages.rb +63 -0
- data/lib/ecoportal/api/v2/register.rb +36 -0
- data/lib/ecoportal/api/v2/registers.rb +35 -0
- data/lib/ecoportal/api/v2/template.rb +10 -0
- data/lib/ecoportal/api/v2/version.rb +7 -0
- 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
|