attr_json 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +17 -0
- data/.yardopts +1 -0
- data/Gemfile +42 -0
- data/LICENSE.txt +21 -0
- data/README.md +426 -0
- data/Rakefile +8 -0
- data/bin/console +23 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/setup +11 -0
- data/config.ru +9 -0
- data/doc_src/dirty_tracking.md +155 -0
- data/doc_src/forms.md +124 -0
- data/json_attribute.gemspec +50 -0
- data/lib/attr_json.rb +18 -0
- data/lib/attr_json/attribute_definition.rb +93 -0
- data/lib/attr_json/attribute_definition/registry.rb +93 -0
- data/lib/attr_json/model.rb +270 -0
- data/lib/attr_json/model/cocoon_compat.rb +27 -0
- data/lib/attr_json/nested_attributes.rb +92 -0
- data/lib/attr_json/nested_attributes/builder.rb +24 -0
- data/lib/attr_json/nested_attributes/multiparameter_attribute_writer.rb +86 -0
- data/lib/attr_json/nested_attributes/writer.rb +215 -0
- data/lib/attr_json/record.rb +140 -0
- data/lib/attr_json/record/dirty.rb +281 -0
- data/lib/attr_json/record/query_builder.rb +84 -0
- data/lib/attr_json/record/query_scopes.rb +35 -0
- data/lib/attr_json/type/array.rb +55 -0
- data/lib/attr_json/type/container_attribute.rb +56 -0
- data/lib/attr_json/type/model.rb +77 -0
- data/lib/attr_json/version.rb +3 -0
- data/playground_models.rb +101 -0
- metadata +177 -0
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'attr_json/attribute_definition'
|
2
|
+
require 'attr_json/attribute_definition/registry'
|
3
|
+
require 'attr_json/type/container_attribute'
|
4
|
+
|
5
|
+
module AttrJson
|
6
|
+
# The mix-in to provide AttrJson support to ActiveRecord::Base models.
|
7
|
+
# We call it `Record` instead of `ActiveRecord` to avoid confusing namespace
|
8
|
+
# shadowing errors, sorry!
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# class SomeModel < ActiveRecord::Base
|
12
|
+
# include AttrJson::Record
|
13
|
+
#
|
14
|
+
# attr_json :a_number, :integer
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
module Record
|
18
|
+
extend ActiveSupport::Concern
|
19
|
+
|
20
|
+
DEFAULT_CONTAINER_ATTRIBUTE = :json_attributes
|
21
|
+
|
22
|
+
included do
|
23
|
+
unless self < ActiveRecord::Base
|
24
|
+
raise TypeError, "AttrJson::Record can only be used with an ActiveRecord::Base model. #{self} does not appear to be one. Are you looking for ::AttrJson::Model?"
|
25
|
+
end
|
26
|
+
|
27
|
+
class_attribute :attr_json_registry, instance_accessor: false
|
28
|
+
self.attr_json_registry = AttrJson::AttributeDefinition::Registry.new
|
29
|
+
|
30
|
+
class_attribute :default_json_container_attribute, instance_acessor: false
|
31
|
+
self.default_json_container_attribute ||= DEFAULT_CONTAINER_ATTRIBUTE
|
32
|
+
end
|
33
|
+
|
34
|
+
class_methods do
|
35
|
+
# Type can be a symbol that will be looked up in `ActiveModel::Type.lookup`,
|
36
|
+
# or an ActiveModel:::Type::Value).
|
37
|
+
#
|
38
|
+
# @param name [Symbol,String] name of attribute
|
39
|
+
#
|
40
|
+
# @param type [ActiveModel::Type::Value] An instance of an ActiveModel::Type::Value (or subclass)
|
41
|
+
#
|
42
|
+
# @option options [Boolean] :array (false) Make this attribute an array of given type.
|
43
|
+
#
|
44
|
+
# @option options [Object] :default (nil) Default value, if a Proc object it will be #call'd
|
45
|
+
# for default.
|
46
|
+
#
|
47
|
+
# @option options [String,Symbol] :store_key (nil) Serialize to JSON using
|
48
|
+
# given store_key, rather than name as would be usual.
|
49
|
+
#
|
50
|
+
# @option options [Symbol,String] :container_attribute (self.default_json_container_attribute) The real
|
51
|
+
# json(b) ActiveRecord attribute/column to serialize as a key in. Defaults to
|
52
|
+
# `self.default_json_container_attribute`, which defaults to `:attr_jsons`
|
53
|
+
#
|
54
|
+
# @option options [Boolean] :validate (true) Create an ActiveRecord::Validations::AssociatedValidator so
|
55
|
+
# validation errors on the attributes post up to self.
|
56
|
+
#
|
57
|
+
# @option options [Boolean] :rails_attribute (false) Create an actual ActiveRecord
|
58
|
+
# `attribute` for name param. A Rails attribute isn't needed for our functionality,
|
59
|
+
# but registering thusly will let the type be picked up by simple_form and
|
60
|
+
# other tools that may look for it via Rails attribute APIs.
|
61
|
+
def attr_json(name, type, **options)
|
62
|
+
options = {
|
63
|
+
rails_attribute: false,
|
64
|
+
validate: true,
|
65
|
+
container_attribute: self.default_json_container_attribute
|
66
|
+
}.merge!(options)
|
67
|
+
options.assert_valid_keys(AttributeDefinition::VALID_OPTIONS + [:validate, :rails_attribute])
|
68
|
+
container_attribute = options[:container_attribute]
|
69
|
+
|
70
|
+
# TODO arg check container_attribute make sure it exists. Hard cause
|
71
|
+
# schema isn't loaded yet when class def is loaded. Maybe not.
|
72
|
+
|
73
|
+
# Want to lazily add an attribute cover to the json container attribute,
|
74
|
+
# only if it hasn't already been done. WARNING we are using internal
|
75
|
+
# Rails API here, but only way to do this lazily, which I thought was
|
76
|
+
# worth it. On the other hand, I think .attribute is idempotent, maybe we don't need it...
|
77
|
+
unless attributes_to_define_after_schema_loads[container_attribute.to_s] &&
|
78
|
+
attributes_to_define_after_schema_loads[container_attribute.to_s].first.is_a?(AttrJson::Type::ContainerAttribute)
|
79
|
+
attribute container_attribute.to_sym, AttrJson::Type::ContainerAttribute.new(self, container_attribute)
|
80
|
+
end
|
81
|
+
|
82
|
+
self.attr_json_registry = attr_json_registry.with(
|
83
|
+
AttributeDefinition.new(name.to_sym, type, options.except(:rails_attribute, :validate))
|
84
|
+
)
|
85
|
+
|
86
|
+
# By default, automatically validate nested models
|
87
|
+
if type.kind_of?(AttrJson::Type::Model) && options[:validate]
|
88
|
+
self.validates_with ActiveRecord::Validations::AssociatedValidator, attributes: [name.to_sym]
|
89
|
+
end
|
90
|
+
|
91
|
+
# We don't actually use this for anything, we provide our own covers. But registering
|
92
|
+
# it with usual system will let simple_form and maybe others find it.
|
93
|
+
if options[:rails_attribute]
|
94
|
+
self.attribute name.to_sym, self.attr_json_registry.fetch(name).type
|
95
|
+
end
|
96
|
+
|
97
|
+
_attr_jsons_module.module_eval do
|
98
|
+
define_method("#{name}=") do |value|
|
99
|
+
attribute_def = self.class.attr_json_registry.fetch(name.to_sym)
|
100
|
+
# write_store_attribute copied from Rails store_accessor implementation.
|
101
|
+
# https://github.com/rails/rails/blob/74c3e43fba458b9b863d27f0c45fd2d8dc603cbc/activerecord/lib/active_record/store.rb#L90-L96
|
102
|
+
|
103
|
+
# special handling for nil, sorry, because if name key was previously
|
104
|
+
# not present, write_store_attribute by default will decide there was
|
105
|
+
# no change and refuse to make the change. TODO messy.
|
106
|
+
if value.nil? && !public_send(attribute_def.container_attribute).has_key?(attribute_def.store_key)
|
107
|
+
public_send :"#{attribute_def.container_attribute}_will_change!"
|
108
|
+
public_send(attribute_def.container_attribute)[attribute_def.store_key] = nil
|
109
|
+
else
|
110
|
+
# use of `write_store_attribute` is copied from Rails store_accessor implementation.
|
111
|
+
# https://github.com/rails/rails/blob/74c3e43fba458b9b863d27f0c45fd2d8dc603cbc/activerecord/lib/active_record/store.rb#L90-L96
|
112
|
+
write_store_attribute(attribute_def.container_attribute, attribute_def.store_key, attribute_def.cast(value))
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
define_method("#{name}") do
|
117
|
+
attribute_def = self.class.attr_json_registry.fetch(name.to_sym)
|
118
|
+
|
119
|
+
# use of `read_store_attribute` is copied from Rails store_accessor implementation.
|
120
|
+
# https://github.com/rails/rails/blob/74c3e43fba458b9b863d27f0c45fd2d8dc603cbc/activerecord/lib/active_record/store.rb#L90-L96
|
121
|
+
read_store_attribute(attribute_def.container_attribute, attribute_def.store_key)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
# Define an anonymous module and include it, so can still be easily
|
129
|
+
# overridden by concrete class. Design cribbed from ActiveRecord::Store
|
130
|
+
# https://github.com/rails/rails/blob/4590d7729e241cb7f66e018a2a9759cb3baa36e5/activerecord/lib/active_record/store.rb
|
131
|
+
def _attr_jsons_module # :nodoc:
|
132
|
+
@_attr_jsons_module ||= begin
|
133
|
+
mod = Module.new
|
134
|
+
include mod
|
135
|
+
mod
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,281 @@
|
|
1
|
+
module AttrJson
|
2
|
+
module Record
|
3
|
+
# This only works in Rails 5.1+, and only uses the 'new style' dirty
|
4
|
+
# tracking methods, available in Rails 5.1+.
|
5
|
+
#
|
6
|
+
# Add into an ActiveRecord object with AttrJson::Record,
|
7
|
+
# to track dirty changes to attr_jsons, off the attr_json_changes
|
8
|
+
# object.
|
9
|
+
#
|
10
|
+
# some_model.attr_json_changes.saved_changes
|
11
|
+
# some_model.attr_json_changes.json_attr_before_last_save
|
12
|
+
#
|
13
|
+
# All methods ordinarily in ActiveRecord::Attributes::Dirty should be available,
|
14
|
+
# including synthetic attribute-specific ones like `will_save_change_to_attribute_name?`.
|
15
|
+
# By default, they _only_ report changes from json attributes.
|
16
|
+
# To have a merged list also including ordinary AR changes, add on `merged`:
|
17
|
+
#
|
18
|
+
# some_model.attr_json_changes.merged.saved_changes
|
19
|
+
# some_model.attr_json_changes.merged.ordinary_attr_before_last_save
|
20
|
+
#
|
21
|
+
# Complex nested models will show up in changes as the cast models. If you want
|
22
|
+
# the raw json instead, use `as_json`:
|
23
|
+
#
|
24
|
+
# some_model.attr_json_changes.as_json.saved_changes
|
25
|
+
#
|
26
|
+
# You can combine as_json and merged if you like:
|
27
|
+
#
|
28
|
+
# some_model.attr_json_changes.as_json.merged.saved_changes
|
29
|
+
#
|
30
|
+
# See more in [separate documentation guide](../../../doc_src/dirty_tracking.md)
|
31
|
+
#
|
32
|
+
# See what methods are available off of the object returned by {attr_json_changes}
|
33
|
+
# in {Dirty::Implementation} -- should be the AR dirty-tracking methods you expect.
|
34
|
+
module Dirty
|
35
|
+
def attr_json_changes
|
36
|
+
Implementation.new(self)
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
class Implementation
|
41
|
+
# The attribute_method stuff is copied from ActiveRecord::Dirty,
|
42
|
+
# to give you all the same synthetic per-attribute methods.
|
43
|
+
# We make it work with overridden #matched_attribute_method below.
|
44
|
+
include ActiveModel::AttributeMethods
|
45
|
+
|
46
|
+
# Attribute methods for "changed in last call to save?"
|
47
|
+
attribute_method_affix(prefix: "saved_change_to_", suffix: "?")
|
48
|
+
attribute_method_prefix("saved_change_to_")
|
49
|
+
attribute_method_suffix("_before_last_save")
|
50
|
+
|
51
|
+
# Attribute methods for "will change if I call save?"
|
52
|
+
attribute_method_affix(prefix: "will_save_change_to_", suffix: "?")
|
53
|
+
attribute_method_suffix("_change_to_be_saved", "_in_database")
|
54
|
+
|
55
|
+
attr_reader :model
|
56
|
+
|
57
|
+
def initialize(model, merged: false, merge_containers: false, as_json: false)
|
58
|
+
@model = model
|
59
|
+
@merged = !!merged
|
60
|
+
@merge_containers = !!merge_containers
|
61
|
+
@as_json = !!as_json
|
62
|
+
end
|
63
|
+
|
64
|
+
# return a copy with `merged` attribute true, so dirty tracking
|
65
|
+
# will include ordinary AR attributes too, and you can do things like:
|
66
|
+
#
|
67
|
+
# model.attr_json_changes.merged.saved_change_to_attribute?(ordinary_or_attr_json)
|
68
|
+
#
|
69
|
+
# By default, the json container attributes are included too. If you
|
70
|
+
# instead want our dirty tracking to pretend they don't exist:
|
71
|
+
#
|
72
|
+
# model.attr_json_changes.merged(containers: false).etc
|
73
|
+
#
|
74
|
+
def merged(containers: true)
|
75
|
+
self.class.new(model, merged: true, merge_containers: containers,
|
76
|
+
as_json: as_json?)
|
77
|
+
end
|
78
|
+
|
79
|
+
# return a copy with as_json parameter set to true, so change diffs
|
80
|
+
# will be the json structures serialized, not the cast models.
|
81
|
+
# for 'primitive' types will be the same, but for AttrJson::Models
|
82
|
+
# very different.
|
83
|
+
def as_json
|
84
|
+
self.class.new(model, as_json: true,
|
85
|
+
merged: merged?,
|
86
|
+
merge_containers: merge_containers?)
|
87
|
+
end
|
88
|
+
|
89
|
+
# should we handle ordinary AR attributes too in one merged
|
90
|
+
# change tracker?
|
91
|
+
def merged?
|
92
|
+
@merged
|
93
|
+
end
|
94
|
+
|
95
|
+
# if we're `merged?` and `merge_containers?` is **false**, we
|
96
|
+
# _omit_ our json container attributes from our dirty tracking.
|
97
|
+
# only has meaning if `merged?` is true. Defaults to true.
|
98
|
+
def merge_containers?
|
99
|
+
@merge_containers
|
100
|
+
end
|
101
|
+
|
102
|
+
def as_json?
|
103
|
+
@as_json
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
def saved_change_to_attribute(attr_name)
|
108
|
+
attribute_def = registry[attr_name.to_sym]
|
109
|
+
if ! attribute_def
|
110
|
+
if merged? && (merge_containers? || ! registry.container_attributes.include?(attr_name.to_s))
|
111
|
+
return model.saved_change_to_attribute(attr_name)
|
112
|
+
else
|
113
|
+
return nil
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
json_container = attribute_def.container_attribute
|
118
|
+
|
119
|
+
(before_container, after_container) = model.saved_change_to_attribute(json_container)
|
120
|
+
|
121
|
+
formatted_before_after(
|
122
|
+
before_container.try(:[], attribute_def.store_key),
|
123
|
+
after_container.try(:[], attribute_def.store_key),
|
124
|
+
attribute_def)
|
125
|
+
end
|
126
|
+
|
127
|
+
def attribute_before_last_save(attr_name)
|
128
|
+
saved_change = saved_change_to_attribute(attr_name)
|
129
|
+
return nil if saved_change.nil?
|
130
|
+
|
131
|
+
saved_change[0]
|
132
|
+
end
|
133
|
+
|
134
|
+
def saved_change_to_attribute?(attr_name)
|
135
|
+
return nil unless registry[attr_name.to_sym] || merged? && (merge_containers? || ! registry.container_attributes.include?(attr_name.to_s))
|
136
|
+
! saved_change_to_attribute(attr_name).nil?
|
137
|
+
end
|
138
|
+
|
139
|
+
def saved_changes
|
140
|
+
saved_changes = model.saved_changes
|
141
|
+
return {} if saved_changes == {}
|
142
|
+
|
143
|
+
json_attr_changes = registry.definitions.collect do |definition|
|
144
|
+
if container_change = saved_changes[definition.container_attribute]
|
145
|
+
old_v = container_change[0][definition.store_key]
|
146
|
+
new_v = container_change[1][definition.store_key]
|
147
|
+
if old_v != new_v
|
148
|
+
[ definition.name.to_s, formatted_before_after(old_v, new_v, definition) ]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end.compact.to_h
|
152
|
+
|
153
|
+
prepared_changes(json_attr_changes, saved_changes)
|
154
|
+
end
|
155
|
+
|
156
|
+
def saved_changes?
|
157
|
+
saved_changes.present?
|
158
|
+
end
|
159
|
+
|
160
|
+
|
161
|
+
def attribute_in_database(attr_name)
|
162
|
+
to_be_saved = attribute_change_to_be_saved(attr_name)
|
163
|
+
if to_be_saved.nil?
|
164
|
+
if merged? && (merge_containers? || ! registry.container_attributes.include?(attr_name.to_s))
|
165
|
+
return model.attribute_change_to_be_saved(attr_name)
|
166
|
+
else
|
167
|
+
return nil
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
to_be_saved[0]
|
172
|
+
end
|
173
|
+
|
174
|
+
def attribute_change_to_be_saved(attr_name)
|
175
|
+
attribute_def = registry[attr_name.to_sym]
|
176
|
+
if ! attribute_def
|
177
|
+
if merged? && (merge_containers? || ! registry.container_attributes.include?(attr_name.to_s))
|
178
|
+
return model.attribute_change_to_be_saved(attr_name)
|
179
|
+
else
|
180
|
+
return nil
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
json_container = attribute_def.container_attribute
|
185
|
+
|
186
|
+
(before_container, after_container) = model.attribute_change_to_be_saved(json_container)
|
187
|
+
|
188
|
+
formatted_before_after(
|
189
|
+
before_container.try(:[], attribute_def.store_key),
|
190
|
+
after_container.try(:[], attribute_def.store_key),
|
191
|
+
attribute_def
|
192
|
+
)
|
193
|
+
end
|
194
|
+
|
195
|
+
def will_save_change_to_attribute?(attr_name)
|
196
|
+
return nil unless registry[attr_name.to_sym] || merged? && (merge_containers? || ! registry.container_attributes.include?(attr_name.to_s))
|
197
|
+
! attribute_change_to_be_saved(attr_name).nil?
|
198
|
+
end
|
199
|
+
|
200
|
+
def changes_to_save
|
201
|
+
changes_to_save = model.changes_to_save
|
202
|
+
|
203
|
+
return {} if changes_to_save == {}
|
204
|
+
|
205
|
+
json_attr_changes = registry.definitions.collect do |definition|
|
206
|
+
if container_change = changes_to_save[definition.container_attribute]
|
207
|
+
old_v = container_change[0][definition.store_key]
|
208
|
+
new_v = container_change[1][definition.store_key]
|
209
|
+
if old_v != new_v
|
210
|
+
[ definition.name.to_s, formatted_before_after(old_v, new_v, definition) ]
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end.compact.to_h
|
214
|
+
|
215
|
+
prepared_changes(json_attr_changes, changes_to_save)
|
216
|
+
end
|
217
|
+
|
218
|
+
def has_changes_to_save?
|
219
|
+
changes_to_save.present?
|
220
|
+
end
|
221
|
+
|
222
|
+
def changed_attribute_names_to_save
|
223
|
+
changes_to_save.keys
|
224
|
+
end
|
225
|
+
|
226
|
+
def attributes_in_database
|
227
|
+
changes_to_save.transform_values(&:first)
|
228
|
+
end
|
229
|
+
|
230
|
+
private
|
231
|
+
|
232
|
+
# returns an array of before and after, possibly formatted with as_json.
|
233
|
+
# if both before and after are nil, returns nil.
|
234
|
+
def formatted_before_after(before_v, after_v, attribute_def)
|
235
|
+
return nil if before_v.nil? && after_v.nil?
|
236
|
+
|
237
|
+
if as_json?
|
238
|
+
before_v = attribute_def.type.serialize(before_v) unless before_v.nil?
|
239
|
+
after_v = attribute_def.type.serialize(after_v) unless after_v.nil?
|
240
|
+
end
|
241
|
+
|
242
|
+
[
|
243
|
+
before_v,
|
244
|
+
after_v
|
245
|
+
]
|
246
|
+
|
247
|
+
end
|
248
|
+
|
249
|
+
# Takes a hash of _our_ attr_json changes, and possibly
|
250
|
+
# merges them into the hash of all changes from the parent record,
|
251
|
+
# depending on values of `merged?` and `merge_containers?`.
|
252
|
+
def prepared_changes(json_attr_changes, all_changes)
|
253
|
+
if merged?
|
254
|
+
all_changes.merge(json_attr_changes).tap do |merged|
|
255
|
+
unless merge_containers?
|
256
|
+
merged.except!(*registry.container_attributes)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
else
|
260
|
+
json_attr_changes
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def registry
|
265
|
+
model.class.attr_json_registry
|
266
|
+
end
|
267
|
+
|
268
|
+
# Override from ActiveModel::AttributeMethods
|
269
|
+
# to not require class-static define_attribute, but instead dynamically
|
270
|
+
# find it from currently declared attributes.
|
271
|
+
# https://github.com/rails/rails/blob/6aa5cf03ea8232180ffbbae4c130b051f813c670/activemodel/lib/active_model/attribute_methods.rb#L463-L468
|
272
|
+
def matched_attribute_method(method_name)
|
273
|
+
matches = self.class.send(:attribute_method_matchers_matching, method_name)
|
274
|
+
matches.detect do |match|
|
275
|
+
registry.has_attribute?(match.attr_name)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module AttrJson
|
2
|
+
module Record
|
3
|
+
# Implementation class called by #jsonb_contains scope method. Ordinarily
|
4
|
+
# you don't need to use it yourself, but you can.
|
5
|
+
class QueryBuilder
|
6
|
+
attr_reader :relation, :input_attributes
|
7
|
+
def initialize(relation, input_attributes)
|
8
|
+
@relation = relation
|
9
|
+
@input_attributes = input_attributes
|
10
|
+
end
|
11
|
+
|
12
|
+
def contains_relation
|
13
|
+
result_relation = relation
|
14
|
+
|
15
|
+
group_attributes_by_container.each do |container_attribute, attributes|
|
16
|
+
param_hash = {}
|
17
|
+
|
18
|
+
attributes.each do |key, value|
|
19
|
+
add_to_param_hash!(param_hash, key, value)
|
20
|
+
end
|
21
|
+
result_relation = result_relation.where("#{relation.table_name}.#{container_attribute} @> (?)::jsonb", param_hash.to_json)
|
22
|
+
end
|
23
|
+
|
24
|
+
result_relation
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def merge_param_hash!(original, new)
|
30
|
+
original.deep_merge!(new) do |key, old_val, new_val|
|
31
|
+
if old_val.is_a?(Array) && old_val.first.is_a?(Hash) && new_val.is_a?(Array) && new_val.first.is_a?(Hash)
|
32
|
+
[merge_param_hash!(old_val.first, new_val.first)]
|
33
|
+
elsif old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
34
|
+
merge_param_hash!(old_val, new_val)
|
35
|
+
else
|
36
|
+
new_val
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def add_to_param_hash!(param_hash, key_path_str, value)
|
43
|
+
key_path = key_path_str.to_s.split(".")
|
44
|
+
first_key, rest_keys = key_path.first, key_path[1..-1]
|
45
|
+
attr_def = relation.attr_json_registry.fetch(first_key)
|
46
|
+
|
47
|
+
value = if rest_keys.present?
|
48
|
+
attr_def.type.value_for_contains_query(rest_keys, value)
|
49
|
+
else
|
50
|
+
attr_def.serialize(attr_def.cast value)
|
51
|
+
end
|
52
|
+
|
53
|
+
if value.kind_of?(Hash)
|
54
|
+
param_hash[attr_def.store_key] ||= {}
|
55
|
+
merge_param_hash!(param_hash[attr_def.store_key], value)
|
56
|
+
else
|
57
|
+
param_hash[attr_def.store_key] = value
|
58
|
+
end
|
59
|
+
|
60
|
+
# it's a mutator not functional don't you forget it.
|
61
|
+
return nil
|
62
|
+
end
|
63
|
+
|
64
|
+
# returns a hash with keys container attributes, values hashes of attributes
|
65
|
+
# belonging to that container attribute.
|
66
|
+
def group_attributes_by_container
|
67
|
+
@group_attributes_by_container ||= begin
|
68
|
+
hash_by_container_attribute = {}
|
69
|
+
|
70
|
+
input_attributes.each do |key_path, value|
|
71
|
+
key = key_path.to_s.split(".").first
|
72
|
+
attr_def = relation.attr_json_registry.fetch(key)
|
73
|
+
container_attribute = attr_def.container_attribute
|
74
|
+
|
75
|
+
hash_by_container_attribute[container_attribute] ||= {}
|
76
|
+
hash_by_container_attribute[container_attribute][key_path] = value
|
77
|
+
end
|
78
|
+
|
79
|
+
hash_by_container_attribute
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|