attr_json 0.1.0
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 +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
|