ecoportal-api 0.8.5 → 0.9.2

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +20 -20
  3. data/.rspec +3 -3
  4. data/.rubocop.yml +55 -55
  5. data/.travis.yml +5 -5
  6. data/.yardopts +10 -10
  7. data/CHANGELOG.md +257 -236
  8. data/Gemfile +6 -6
  9. data/LICENSE +21 -21
  10. data/README.md +34 -34
  11. data/Rakefile +27 -27
  12. data/bin/console +14 -14
  13. data/bin/setup +8 -8
  14. data/ecoportal-api.gemspec +36 -36
  15. data/lib/ecoportal/api/common/base_class.rb +33 -29
  16. data/lib/ecoportal/api/common/base_model.rb +195 -177
  17. data/lib/ecoportal/api/common/batch_operation.rb +119 -119
  18. data/lib/ecoportal/api/common/batch_response.rb +34 -34
  19. data/lib/ecoportal/api/common/client.rb +198 -196
  20. data/lib/ecoportal/api/common/doc_helpers.rb +29 -29
  21. data/lib/ecoportal/api/common/elastic_apm_integration.rb +112 -112
  22. data/lib/ecoportal/api/common/hash_diff.rb +41 -41
  23. data/lib/ecoportal/api/common/logging.rb +12 -12
  24. data/lib/ecoportal/api/common/response.rb +31 -31
  25. data/lib/ecoportal/api/common/wrapped_response.rb +54 -54
  26. data/lib/ecoportal/api/common.rb +18 -18
  27. data/lib/ecoportal/api/errors/base.rb +8 -8
  28. data/lib/ecoportal/api/errors/time_out.rb +8 -8
  29. data/lib/ecoportal/api/errors.rb +9 -9
  30. data/lib/ecoportal/api/internal/account.rb +99 -100
  31. data/lib/ecoportal/api/internal/login_provider.rb +9 -9
  32. data/lib/ecoportal/api/internal/login_providers.rb +33 -33
  33. data/lib/ecoportal/api/internal/people.rb +14 -14
  34. data/lib/ecoportal/api/internal/permissions.rb +14 -13
  35. data/lib/ecoportal/api/internal/person.rb +101 -53
  36. data/lib/ecoportal/api/internal/person_details.rb +9 -9
  37. data/lib/ecoportal/api/internal/person_schema.rb +10 -10
  38. data/lib/ecoportal/api/internal/person_schemas.rb +11 -11
  39. data/lib/ecoportal/api/internal/policy_group.rb +9 -9
  40. data/lib/ecoportal/api/internal/policy_groups.rb +32 -32
  41. data/lib/ecoportal/api/internal/preferences.rb +31 -31
  42. data/lib/ecoportal/api/internal/schema_field.rb +8 -8
  43. data/lib/ecoportal/api/internal/schema_field_value.rb +8 -8
  44. data/lib/ecoportal/api/internal.rb +31 -31
  45. data/lib/ecoportal/api/logger.rb +62 -62
  46. data/lib/ecoportal/api/v1/people.rb +218 -218
  47. data/lib/ecoportal/api/v1/person.rb +138 -135
  48. data/lib/ecoportal/api/v1/person_details.rb +94 -82
  49. data/lib/ecoportal/api/v1/person_schema.rb +53 -53
  50. data/lib/ecoportal/api/v1/person_schemas.rb +48 -48
  51. data/lib/ecoportal/api/v1/schema_field.rb +34 -34
  52. data/lib/ecoportal/api/v1/schema_field_value.rb +65 -65
  53. data/lib/ecoportal/api/v1.rb +49 -49
  54. data/lib/ecoportal/api/version.rb +5 -5
  55. data/lib/ecoportal/api.rb +16 -16
  56. metadata +3 -3
data/Rakefile CHANGED
@@ -1,27 +1,27 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
3
- require "yard"
4
- require "redcarpet"
5
-
6
- desc "run the specs"
7
- RSpec::Core::RakeTask.new(:spec)
8
-
9
- desc "run rspec showing backtrace"
10
- RSpec::Core::RakeTask.new(:spec_trace) do |task|
11
- task.rspec_opts = ['--backtrace']
12
- end
13
-
14
- desc "run rspec stopping on first fail, and show backtrace"
15
- RSpec::Core::RakeTask.new(:spec_fast) do |task|
16
- task.rspec_opts = ['--fail-fast', '--backtrace']
17
- end
18
-
19
- # default task name is yard
20
- desc "Yard: generate all the documentation"
21
- YARD::Rake::YardocTask.new(:doc) do |t|
22
- #t.files = ['lib/**/*.rb']
23
- end
24
-
25
- task :default => [:spec]
26
- task :rspec_trace => :spec_trace
27
- task :rspec_fast => :spec_fast
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ require "yard"
4
+ require "redcarpet"
5
+
6
+ desc "run the specs"
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ desc "run rspec showing backtrace"
10
+ RSpec::Core::RakeTask.new(:spec_trace) do |task|
11
+ task.rspec_opts = ['--backtrace']
12
+ end
13
+
14
+ desc "run rspec stopping on first fail, and show backtrace"
15
+ RSpec::Core::RakeTask.new(:spec_fast) do |task|
16
+ task.rspec_opts = ['--fail-fast', '--backtrace']
17
+ end
18
+
19
+ # default task name is yard
20
+ desc "Yard: generate all the documentation"
21
+ YARD::Rake::YardocTask.new(:doc) do |t|
22
+ #t.files = ['lib/**/*.rb']
23
+ end
24
+
25
+ task :default => [:spec]
26
+ task :rspec_trace => :spec_trace
27
+ task :rspec_fast => :spec_fast
data/bin/console CHANGED
@@ -1,14 +1,14 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "ecoportal/api"
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "pry"
14
- Pry.start(__FILE__)
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ecoportal/api"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "pry"
14
+ Pry.start(__FILE__)
data/bin/setup CHANGED
@@ -1,8 +1,8 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -1,36 +1,36 @@
1
-
2
- lib = File.expand_path("../lib", __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "ecoportal/api/version"
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "ecoportal-api"
8
- spec.version = Ecoportal::API::VERSION
9
- spec.authors = ["Tapio Saarinen"]
10
- spec.email = ["tapio@ecoportal.co.nz", "rien@ecoportal.co.nz", "oscar@ecoportal.co.nz", "bozydar@ecoportal.co.nz"]
11
-
12
- spec.summary = %q{A collection of helpers for interacting with the ecoPortal MS's various APIs}
13
- spec.homepage = "https://www.ecoportal.com"
14
- spec.licenses = %w[MIT]
15
-
16
- spec.required_ruby_version = '>= 2.4.4'
17
-
18
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
- f.match(%r{^(test|spec|features)/})
20
- end
21
- spec.bindir = "exe"
22
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
- spec.require_paths = ["lib"]
24
-
25
- spec.add_development_dependency "bundler", ">= 2.2.17", "< 2.3"
26
- spec.add_development_dependency "rspec", ">= 3.10.0", "< 3.11"
27
- spec.add_development_dependency "rake", ">= 13.0.3", "< 13.1"
28
- spec.add_development_dependency "yard", ">= 0.9.26", "< 0.10"
29
- spec.add_development_dependency "redcarpet", ">= 3.5.1", "< 3.6"
30
- spec.add_development_dependency "pry" , "~> 0.14"
31
-
32
- spec.add_dependency 'http', '~> 4.4.1', "< 5"
33
- spec.add_dependency 'dotenv', '>= 2.7.6', "< 2.8"
34
- spec.add_dependency 'elastic-apm', '>= 4.0.0', "< 4.1"
35
- spec.add_dependency 'hash-polyfill', '~> 0'
36
- end
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "ecoportal/api/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ecoportal-api"
8
+ spec.version = Ecoportal::API::VERSION
9
+ spec.authors = ["Tapio Saarinen"]
10
+ spec.email = ["tapio@ecoportal.co.nz", "rien@ecoportal.co.nz", "oscar@ecoportal.co.nz", "bozydar@ecoportal.co.nz"]
11
+
12
+ spec.summary = %q{A collection of helpers for interacting with the ecoPortal MS's various APIs}
13
+ spec.homepage = "https://www.ecoportal.com"
14
+ spec.licenses = %w[MIT]
15
+
16
+ spec.required_ruby_version = '>= 2.4.4'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
+ f.match(%r{^(test|spec|features)/})
20
+ end
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_development_dependency "bundler", ">= 2.2.17", "< 2.3"
26
+ spec.add_development_dependency "rspec", ">= 3.10.0", "< 3.11"
27
+ spec.add_development_dependency "rake", ">= 13.0.3", "< 13.1"
28
+ spec.add_development_dependency "yard", ">= 0.9.26", "< 0.10"
29
+ spec.add_development_dependency "redcarpet", ">= 3.5.1", "< 3.6"
30
+ spec.add_development_dependency "pry" , "~> 0.14"
31
+
32
+ spec.add_dependency 'http', '~> 4.4.1', "< 5"
33
+ spec.add_dependency 'dotenv', '>= 2.7.6', "< 2.8"
34
+ spec.add_dependency 'elastic-apm', '>= 4.0.0', "< 4.1"
35
+ spec.add_dependency 'hash-polyfill', '~> 0'
36
+ end
@@ -1,29 +1,33 @@
1
- module Ecoportal
2
- module API
3
- module Common
4
- module BaseClass
5
-
6
- def class_resolver(name, klass)
7
- define_singleton_method(name) { resolve_class(klass) }
8
- define_method(name) { self.class.resolve_class(klass) }
9
- end
10
-
11
- def resolve_class(klass)
12
- @resolved ||= {}
13
- @resolved[klass] ||=
14
- case klass
15
- when Class
16
- klass
17
- when String
18
- Kernel.const_get(klass)
19
- when Symbol
20
- resolve_class(self.send(klass))
21
- else
22
- raise "Unknown class: #{klass}"
23
- end
24
- end
25
-
26
- end
27
- end
28
- end
29
- end
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ module BaseClass
5
+ def redef_without_warning(const, value)
6
+ self.class.send(:remove_const, const) if self.class.const_defined?(const)
7
+ self.class.const_set(const, value)
8
+ end
9
+
10
+ def class_resolver(name, klass)
11
+ define_singleton_method(name) { resolve_class(klass) }
12
+ define_method(name) { self.class.resolve_class(klass) }
13
+ end
14
+
15
+ def resolve_class(klass)
16
+ @resolved ||= {}
17
+ @resolved[klass] ||=
18
+ case klass
19
+ when Class
20
+ klass
21
+ when String
22
+ Kernel.const_get(klass)
23
+ when Symbol
24
+ resolve_class(self.send(klass))
25
+ else
26
+ raise "Unknown class: #{klass}"
27
+ end
28
+ end
29
+
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,177 +1,195 @@
1
- module Ecoportal
2
- module API
3
- module Common
4
- class BaseModel
5
- class UnlinkedModel < Exception
6
- def initialize (msg = "Something went wrong when linking the document.", from: nil, key: nil)
7
- msg += " From: #{from}." if from
8
- msg += " key: #{key}." if key
9
- super(msg)
10
- end
11
- end
12
-
13
- extend BaseClass
14
-
15
- class << self
16
- def passthrough(*methods, to: :doc)
17
- methods.each do |method|
18
- method = method.to_s
19
- define_method method do
20
- send(to)[method]
21
- end
22
- define_method "#{method}=" do |value|
23
- send(to)[method] = value
24
- end
25
- end
26
- end
27
-
28
- def embeds_one(method, key: method, nullable: false, klass:)
29
- method = method.to_s.freeze
30
- var = "@#{method}".freeze
31
- key = key.to_s.freeze
32
- define_method(method) do
33
- if instance_variable_defined?(var)
34
- value = instance_variable_get(var)
35
- return value unless nullable
36
- return value if (value && doc[key]) || (!value && !doc[key])
37
- remove_instance_variable(var)
38
- end
39
- doc[key] ||= {} unless nullable
40
- return instance_variable_set(var, nil) unless doc[key]
41
-
42
- self.class.resolve_class(klass).new(
43
- doc[key], parent: self, key: key
44
- ).tap {|obj| instance_variable_set(var, obj)}
45
- end
46
- end
47
-
48
- end
49
-
50
- attr_reader :_parent, :_key
51
-
52
- def initialize(doc = {}, parent: self, key: nil)
53
- @_parent = parent
54
- @_key = key
55
- if !_parent || !_key
56
- @doc = doc
57
- @original_doc = JSON.parse(@doc.to_json)
58
- @initial_doc = JSON.parse(@doc.to_json)
59
- end
60
- end
61
-
62
- def doc
63
- raise UnlinkedModel.new(from: "#{self.class}#doc", key: _key) unless linked?
64
- return @doc if is_root?
65
- _parent.doc.dig(*[_key].flatten)
66
- end
67
-
68
- def original_doc
69
- raise UnlinkedModel.new(from: "#{self.class}#original_doc", key: _key) unless linked?
70
- return @original_doc if is_root?
71
- _parent.original_doc&.dig(*[_key].flatten)
72
- end
73
-
74
- def initial_doc
75
- raise UnlinkedModel.new(from: "#{self.class}#initial_doc", key: _key) unless linked?
76
- return @initial_doc if is_root?
77
- _parent.initial_doc&.dig(*[_key].flatten)
78
- end
79
-
80
- def as_json
81
- doc
82
- end
83
-
84
- def to_json(*args)
85
- doc.to_json(*args)
86
- end
87
-
88
- def as_update(ref = :last, ignore: [])
89
- new_doc = as_json
90
- ref_doc = ref == :total ? initial_doc : original_doc
91
- Common::HashDiff.diff(new_doc, ref_doc, ignore: ignore)
92
- end
93
-
94
- def dirty?
95
- as_update != {}
96
- end
97
-
98
- # It consolidates all the changes carried by `doc` by setting it as `original_doc`.
99
- def consolidate!
100
- raise UnlinkedModel.new(from: "#{self.class}#consolidate!", key: _key) unless linked?
101
- new_doc = JSON.parse(doc.to_json)
102
- if is_root?
103
- @original_doc = new_doc
104
- else
105
- dig_set(_parent.original_doc, [_key].flatten, new_doc)
106
- end
107
- end
108
-
109
- # It removes all the changes carried by `doc` by restoring `original_doc` into `doc`.
110
- # @note
111
- # 1. When there are nullable properties, it may be required to apply `reset!` from the parent
112
- # i.e. `parent.reset!("child")` # when parent.child is `nil`
113
- # 2. In such a case, only immediate childs are allowed to be reset
114
- # @param key [String, Array<String>, nil] if given, it only resets the specified property
115
- def reset!(key = nil)
116
- raise "'key' should be a String. Given #{key}" unless !key || key.is_a?(String)
117
- raise UnlinkedModel.new(from: "#{self.class}#reset!", key: _key) unless linked?
118
-
119
- if key
120
- if self.respond_to?(key) && child = self.send(key) && child.is_a?(Ecoportal::API::Common::BaseModel)
121
- child.reset!
122
- else
123
- new_doc = original_doc && original_doc[key]
124
- dig_set(doc, [key], new_doc && JSON.parse(new_doc.to_json))
125
- # regenerate object if new_doc is null
126
- self.send(key) if !new_doc && self.respond_to?(key)
127
- end
128
- else
129
- new_doc = JSON.parse(original_doc.to_json)
130
- if is_root?
131
- @doc = new_doc
132
- else
133
- dig_set(_parent.doc, [_key].flatten, new_doc)
134
- end
135
- end
136
- end
137
-
138
- def print_pretty
139
- puts JSON.pretty_generate(as_json)
140
- self
141
- end
142
-
143
- protected
144
-
145
- def is_root?
146
- _parent == self && !!defined?(@doc)
147
- end
148
-
149
- def linked?
150
- is_root? || !!_parent.doc.dig(*[_key].flatten)
151
- end
152
-
153
- private
154
-
155
- def dig_set(obj, keys, value)
156
- if keys.length == 1
157
- obj[keys.first] = value
158
- else
159
- dig_set(obj[keys.first], keys.slice(1..-1), value)
160
- end
161
- end
162
-
163
- def set_uniq_array_keep_order(key, value)
164
- unless value.is_a?(Array)
165
- raise "#{key}= needs to be passed an Array, got #{value.class}"
166
- end
167
- ini_vals = (original_doc && original_doc[key]) || []
168
-
169
- value = value.uniq
170
- # preserve original order to avoid false updates
171
- doc[key] = ((ini_vals & value) + (value - ini_vals)).compact
172
- end
173
-
174
- end
175
- end
176
- end
177
- end
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ class BaseModel
5
+ class UnlinkedModel < Exception
6
+ def initialize (msg = "Something went wrong when linking the document.", from: nil, key: nil)
7
+ msg += " From: #{from}." if from
8
+ msg += " key: #{key}." if key
9
+ super(msg)
10
+ end
11
+ end
12
+
13
+ extend BaseClass
14
+
15
+ class << self
16
+ def passthrough(*methods, to: :doc)
17
+ methods.each do |method|
18
+ method = method.to_s
19
+ define_method method do
20
+ send(to)[method]
21
+ end
22
+ define_method "#{method}=" do |value|
23
+ send(to)[method] = value
24
+ end
25
+ end
26
+ end
27
+
28
+ def embeds_one(method, key: method, nullable: false, klass:)
29
+ method = method.to_s.freeze
30
+ var = "@#{method}".freeze
31
+ key = key.to_s.freeze
32
+ define_method(method) do
33
+ if instance_variable_defined?(var)
34
+ value = instance_variable_get(var)
35
+ return value unless nullable
36
+ return value if (value && doc[key]) || (!value && !doc[key])
37
+ remove_instance_variable(var)
38
+ end
39
+ doc[key] ||= {} unless nullable
40
+ return instance_variable_set(var, nil) unless doc[key]
41
+
42
+ self.class.resolve_class(klass).new(
43
+ doc[key], parent: self, key: key
44
+ ).tap {|obj| instance_variable_set(var, obj)}
45
+ end
46
+ end
47
+
48
+ end
49
+
50
+ attr_reader :_parent, :_key
51
+
52
+ def initialize(doc = {}, parent: self, key: nil)
53
+ @_parent = parent
54
+ @_key = key
55
+ if !_parent || !_key
56
+ @doc = doc
57
+ @original_doc = JSON.parse(@doc.to_json)
58
+ @initial_doc = JSON.parse(@doc.to_json)
59
+ end
60
+ end
61
+
62
+ def doc
63
+ raise UnlinkedModel.new(from: "#{self.class}#doc", key: _key) unless linked?
64
+ return @doc if is_root?
65
+ _parent.doc.dig(*[_key].flatten)
66
+ end
67
+
68
+ def original_doc
69
+ raise UnlinkedModel.new(from: "#{self.class}#original_doc", key: _key) unless linked?
70
+ return @original_doc if is_root?
71
+ _parent.original_doc&.dig(*[_key].flatten)
72
+ end
73
+
74
+ def initial_doc
75
+ raise UnlinkedModel.new(from: "#{self.class}#initial_doc", key: _key) unless linked?
76
+ return @initial_doc if is_root?
77
+ _parent.initial_doc&.dig(*[_key].flatten)
78
+ end
79
+
80
+ # It replaces `doc` by `new_doc`
81
+ # @return [Hash] `doc` before change
82
+ def replace_doc!(new_doc)
83
+ raise UnlinkedModel.new(from: "#{self.class}#replace_doc", key: _key) unless linked?
84
+ @doc.tap do
85
+ @doc = new_doc
86
+ end
87
+ end
88
+
89
+ # It replaces `original_doc` by `new_doc`
90
+ # @return [Hash] `original_doc` before change
91
+ def replace_original_doc!(new_doc)
92
+ raise UnlinkedModel.new(from: "#{self.class}#replace_original_doc", key: _key) unless linked?
93
+ @original_doc.tap do
94
+ @original_doc = new_doc
95
+ end
96
+ end
97
+
98
+ def as_json
99
+ doc
100
+ end
101
+
102
+ def to_json(*args)
103
+ doc.to_json(*args)
104
+ end
105
+
106
+ def as_update(ref = :last, ignore: [])
107
+ new_doc = as_json
108
+ ref_doc = ref == :total ? initial_doc : original_doc
109
+ Common::HashDiff.diff(new_doc, ref_doc, ignore: ignore)
110
+ end
111
+
112
+ def dirty?
113
+ as_update != {}
114
+ end
115
+
116
+ # It consolidates all the changes carried by `doc` by setting it as `original_doc`.
117
+ def consolidate!
118
+ raise UnlinkedModel.new(from: "#{self.class}#consolidate!", key: _key) unless linked?
119
+ new_doc = JSON.parse(doc.to_json)
120
+ if is_root?
121
+ @original_doc = new_doc
122
+ else
123
+ dig_set(_parent.original_doc, [_key].flatten, new_doc)
124
+ end
125
+ end
126
+
127
+ # It removes all the changes carried by `doc` by restoring `original_doc` into `doc`.
128
+ # @note
129
+ # 1. When there are nullable properties, it may be required to apply `reset!` from the parent
130
+ # i.e. `parent.reset!("child")` # when parent.child is `nil`
131
+ # 2. In such a case, only immediate childs are allowed to be reset
132
+ # @param key [String, Array<String>, nil] if given, it only resets the specified property
133
+ def reset!(key = nil)
134
+ raise "'key' should be a String. Given #{key}" unless !key || key.is_a?(String)
135
+ raise UnlinkedModel.new(from: "#{self.class}#reset!", key: _key) unless linked?
136
+
137
+ if key
138
+ if self.respond_to?(key) && child = self.send(key) && child.is_a?(Ecoportal::API::Common::BaseModel)
139
+ child.reset!
140
+ else
141
+ new_doc = original_doc && original_doc[key]
142
+ dig_set(doc, [key], new_doc && JSON.parse(new_doc.to_json))
143
+ # regenerate object if new_doc is null
144
+ self.send(key) if !new_doc && self.respond_to?(key)
145
+ end
146
+ else
147
+ new_doc = JSON.parse(original_doc.to_json)
148
+ if is_root?
149
+ @doc = new_doc
150
+ else
151
+ dig_set(_parent.doc, [_key].flatten, new_doc)
152
+ end
153
+ end
154
+ end
155
+
156
+ def print_pretty
157
+ puts JSON.pretty_generate(as_json)
158
+ self
159
+ end
160
+
161
+ protected
162
+
163
+ def is_root?
164
+ _parent == self && !!defined?(@doc)
165
+ end
166
+
167
+ def linked?
168
+ is_root? || !!_parent.doc.dig(*[_key].flatten)
169
+ end
170
+
171
+ private
172
+
173
+ def dig_set(obj, keys, value)
174
+ if keys.length == 1
175
+ obj[keys.first] = value
176
+ else
177
+ dig_set(obj[keys.first], keys.slice(1..-1), value)
178
+ end
179
+ end
180
+
181
+ def set_uniq_array_keep_order(key, value)
182
+ unless value.is_a?(Array)
183
+ raise "#{key}= needs to be passed an Array, got #{value.class}"
184
+ end
185
+ ini_vals = (original_doc && original_doc[key]) || []
186
+
187
+ value = value.uniq
188
+ # preserve original order to avoid false updates
189
+ doc[key] = ((ini_vals & value) + (value - ini_vals)).compact
190
+ end
191
+
192
+ end
193
+ end
194
+ end
195
+ end