attr_json 0.2.0 → 1.5.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 +5 -5
- data/.github/workflows/ci.yml +87 -0
- data/.github/workflows/future_rails_ci.yml +66 -0
- data/.gitignore +1 -0
- data/Appraisals +62 -0
- data/CHANGELOG.md +67 -0
- data/Gemfile +38 -35
- data/README.md +161 -36
- data/attr_json.gemspec +27 -4
- data/doc_src/dirty_tracking.md +1 -1
- data/doc_src/forms.md +76 -14
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/rails_5_0.gemfile +20 -0
- data/gemfiles/rails_5_1.gemfile +19 -0
- data/gemfiles/rails_5_2.gemfile +19 -0
- data/gemfiles/rails_6_0.gemfile +19 -0
- data/gemfiles/rails_6_1.gemfile +19 -0
- data/gemfiles/rails_7_0.gemfile +19 -0
- data/gemfiles/rails_edge.gemfile +19 -0
- data/lib/attr_json/attribute_definition/registry.rb +6 -1
- data/lib/attr_json/attribute_definition.rb +15 -2
- data/lib/attr_json/config.rb +55 -0
- data/lib/attr_json/model.rb +125 -42
- data/lib/attr_json/nested_attributes/writer.rb +26 -1
- data/lib/attr_json/nested_attributes.rb +7 -1
- data/lib/attr_json/record/dirty.rb +19 -13
- data/lib/attr_json/record/query_builder.rb +15 -3
- data/lib/attr_json/record/query_scopes.rb +6 -0
- data/lib/attr_json/record.rb +120 -32
- data/lib/attr_json/serialization_coder_from_type.rb +40 -0
- data/lib/attr_json/type/array.rb +6 -0
- data/lib/attr_json/type/container_attribute.rb +14 -3
- data/lib/attr_json/type/model.rb +38 -8
- data/lib/attr_json/type/polymorphic_model.rb +7 -0
- data/lib/attr_json/version.rb +1 -1
- data/lib/attr_json.rb +1 -1
- metadata +61 -21
- data/.travis.yml +0 -17
@@ -12,7 +12,9 @@
|
|
12
12
|
attr_reader :name, :type, :original_args, :container_attribute
|
13
13
|
|
14
14
|
# @param name [Symbol,String]
|
15
|
-
# @param type [Symbol,ActiveModel::Type::Value]
|
15
|
+
# @param type [Symbol,ActiveModel::Type::Value] Symbol is looked up in
|
16
|
+
# ActiveRecord::Type.lookup, but with `adapter: nil` for no custom
|
17
|
+
# adapter-specific lookup.
|
16
18
|
#
|
17
19
|
# @option options store_key [Symbol,String]
|
18
20
|
# @option options container_attribute [Symbol,ActiveModel::Type::Value]
|
@@ -40,7 +42,12 @@
|
|
40
42
|
# ActiveModel::Type.lookup may make more sense, but ActiveModel::Type::Date
|
41
43
|
# seems to have a bug with multi-param assignment. Mostly they return
|
42
44
|
# the same types, but ActiveRecord::Type::Date works with multi-param assignment.
|
43
|
-
|
45
|
+
#
|
46
|
+
# We pass `adapter: nil` to avoid triggering a db connection.
|
47
|
+
# See: https://github.com/jrochkind/attr_json/issues/41
|
48
|
+
# This is at the "cost" of not using any adapter-specific types... which
|
49
|
+
# maybe preferable anyway?
|
50
|
+
type = ActiveRecord::Type.lookup(type, adapter: nil)
|
44
51
|
elsif ! type.is_a? ActiveModel::Type::Value
|
45
52
|
raise ArgumentError, "Second argument (#{type}) must be a symbol or instance of an ActiveModel::Type::Value subclass"
|
46
53
|
end
|
@@ -71,6 +78,12 @@
|
|
71
78
|
@default != NO_DEFAULT_PROVIDED
|
72
79
|
end
|
73
80
|
|
81
|
+
# Can be value or proc!
|
82
|
+
def default_argument
|
83
|
+
return nil unless has_default?
|
84
|
+
@default
|
85
|
+
end
|
86
|
+
|
74
87
|
def provide_default!
|
75
88
|
unless has_default?
|
76
89
|
raise ArgumentError.new("This #{self.class.name} does not have a default defined!")
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module AttrJson
|
2
|
+
# Intentionally non-mutable, to avoid problems with subclass inheritance
|
3
|
+
# and rails class_attribute. Instead, you set to new Config object
|
4
|
+
# changed with {#merge}.
|
5
|
+
class Config
|
6
|
+
RECORD_ALLOWED_KEYS = %i{
|
7
|
+
default_container_attribute
|
8
|
+
default_rails_attribute
|
9
|
+
default_accepts_nested_attributes
|
10
|
+
}
|
11
|
+
|
12
|
+
MODEL_ALLOWED_KEYS = %i{
|
13
|
+
unknown_key
|
14
|
+
bad_cast
|
15
|
+
}
|
16
|
+
|
17
|
+
DEFAULTS = {
|
18
|
+
default_container_attribute: "json_attributes",
|
19
|
+
default_rails_attribute: false,
|
20
|
+
unknown_key: :raise
|
21
|
+
}
|
22
|
+
|
23
|
+
(MODEL_ALLOWED_KEYS + RECORD_ALLOWED_KEYS).each do |key|
|
24
|
+
define_method(key) do
|
25
|
+
attributes[key]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :mode
|
30
|
+
|
31
|
+
def initialize(options = {})
|
32
|
+
@mode = options.delete(:mode)
|
33
|
+
unless mode == :record || mode == :model
|
34
|
+
raise ArgumentError, "required :mode argument must be :record or :model"
|
35
|
+
end
|
36
|
+
valid_keys = mode == :record ? RECORD_ALLOWED_KEYS : MODEL_ALLOWED_KEYS
|
37
|
+
options.assert_valid_keys(valid_keys)
|
38
|
+
|
39
|
+
options.reverse_merge!(DEFAULTS.slice(*valid_keys))
|
40
|
+
|
41
|
+
@attributes = options
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns a new Config object, with changes merged in.
|
45
|
+
def merge(changes = {})
|
46
|
+
self.class.new(attributes.merge(changes).merge(mode: mode))
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def attributes
|
52
|
+
@attributes
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/attr_json/model.rb
CHANGED
@@ -7,6 +7,8 @@ require 'attr_json/attribute_definition/registry'
|
|
7
7
|
require 'attr_json/type/model'
|
8
8
|
require 'attr_json/model/cocoon_compat'
|
9
9
|
|
10
|
+
require 'attr_json/serialization_coder_from_type'
|
11
|
+
|
10
12
|
module AttrJson
|
11
13
|
|
12
14
|
# Meant for use in a plain class, turns it into an ActiveModel::Model
|
@@ -21,12 +23,48 @@ module AttrJson
|
|
21
23
|
# @note Includes ActiveModel::Model whether you like it or not. TODO, should it?
|
22
24
|
#
|
23
25
|
# You can control what happens if you set an unknown key (one that you didn't
|
24
|
-
# register with `attr_json`) with the
|
26
|
+
# register with `attr_json`) with the config attribute `attr_json_config(unknown_key:)`.
|
25
27
|
# * :raise (default) raise ActiveModel::UnknownAttributeError
|
26
28
|
# * :strip Ignore the unknown key and do not include it, without raising.
|
27
29
|
# * :allow Allow the unknown key and it's value to be in the serialized hash,
|
28
30
|
# and written to the database. May be useful for legacy data or columns
|
29
31
|
# that other software touches, to let unknown keys just flow through.
|
32
|
+
#
|
33
|
+
# class Something
|
34
|
+
# include AttrJson::Model
|
35
|
+
# attr_json_config(unknown_key: :allow)
|
36
|
+
# #...
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# Similarly, trying to set a Model-valued attribute with an object that
|
40
|
+
# can't be cast to a Hash or Model at all will normally raise a
|
41
|
+
# AttrJson::Type::Model::BadCast error, but you can set config `bad_cast: :as_nil`
|
42
|
+
# to make it cast to nil, more like typical ActiveRecord cast.
|
43
|
+
#
|
44
|
+
# class Something
|
45
|
+
# include AttrJson::Model
|
46
|
+
# attr_json_config(bad_cast: :as_nil)
|
47
|
+
# #...
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# ## ActiveRecord `serialize`
|
51
|
+
#
|
52
|
+
# If you want to map a single AttrJson::Model to a json/jsonb column, you
|
53
|
+
# can use ActiveRecord `serialize` feature.
|
54
|
+
#
|
55
|
+
# https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html
|
56
|
+
#
|
57
|
+
# We provide a simple shim to give you the right API for a "coder" for AR serialize:
|
58
|
+
#
|
59
|
+
# class ValueModel
|
60
|
+
# include AttrJson::Model
|
61
|
+
# attr_json :some_string, :string
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# class SomeModel < ApplicationRecord
|
65
|
+
# serialize :some_json_column, ValueModel.to_serialize_coder
|
66
|
+
# end
|
67
|
+
#
|
30
68
|
module Model
|
31
69
|
extend ActiveSupport::Concern
|
32
70
|
|
@@ -41,23 +79,51 @@ module AttrJson
|
|
41
79
|
|
42
80
|
class_attribute :attr_json_registry, instance_accessor: false
|
43
81
|
self.attr_json_registry = ::AttrJson::AttributeDefinition::Registry.new
|
44
|
-
|
45
|
-
# :raise, :strip, :allow. :raise is default. Is there some way to enforce this.
|
46
|
-
class_attribute :attr_json_unknown_key
|
47
|
-
self.attr_json_unknown_key ||= :raise
|
48
82
|
end
|
49
83
|
|
50
84
|
class_methods do
|
51
|
-
|
85
|
+
def attr_json_config(new_values = {})
|
86
|
+
if new_values.present?
|
87
|
+
# get one without new values, then merge new values into it, and
|
88
|
+
# set it locally for this class.
|
89
|
+
@attr_json_config = attr_json_config.merge(new_values)
|
90
|
+
else
|
91
|
+
if instance_variable_defined?("@attr_json_config")
|
92
|
+
# we have a custom one for this class, return it.
|
93
|
+
@attr_json_config
|
94
|
+
elsif superclass.respond_to?(:attr_json_config)
|
95
|
+
# return superclass without setting it locally, so changes in superclass
|
96
|
+
# will continue effecting us.
|
97
|
+
superclass.attr_json_config
|
98
|
+
else
|
99
|
+
# no superclass, no nothing, set it to blank one.
|
100
|
+
@attr_json_config = Config.new(mode: :model)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
# The inverse of model#serializable_hash -- re-hydrates a serialized hash to a model.
|
107
|
+
#
|
108
|
+
# Similar to `.new`, but translates things that need to be translated in deserialization,
|
109
|
+
# like store_keys, and properly calling deserialize on the underlying types.
|
110
|
+
#
|
111
|
+
# @example Model.new_from_serializable(hash)
|
52
112
|
def new_from_serializable(attributes = {})
|
53
|
-
attributes = attributes.
|
113
|
+
attributes = attributes.collect do |key, value|
|
54
114
|
# store keys in arguments get translated to attribute names on initialize.
|
55
115
|
if attribute_def = self.attr_json_registry.store_key_lookup("", key.to_s)
|
56
|
-
attribute_def.name.to_s
|
57
|
-
else
|
58
|
-
key
|
116
|
+
key = attribute_def.name.to_s
|
59
117
|
end
|
60
|
-
|
118
|
+
|
119
|
+
attr_type = self.attr_json_registry.has_attribute?(key) && self.attr_json_registry.type_for_attribute(key)
|
120
|
+
if attr_type
|
121
|
+
value = attr_type.deserialize(value)
|
122
|
+
end
|
123
|
+
|
124
|
+
[key, value]
|
125
|
+
end.to_h
|
126
|
+
|
61
127
|
self.new(attributes)
|
62
128
|
end
|
63
129
|
|
@@ -65,6 +131,21 @@ module AttrJson
|
|
65
131
|
@type ||= AttrJson::Type::Model.new(self)
|
66
132
|
end
|
67
133
|
|
134
|
+
def to_serialization_coder
|
135
|
+
@serialization_coder ||= AttrJson::SerializationCoderFromType.new(to_type)
|
136
|
+
end
|
137
|
+
|
138
|
+
# like the ActiveModel::Attributes method
|
139
|
+
def attribute_names
|
140
|
+
attr_json_registry.attribute_names
|
141
|
+
end
|
142
|
+
|
143
|
+
# like the ActiveModel::Attributes method, hash with name keys, and ActiveModel::Type values
|
144
|
+
def attribute_types
|
145
|
+
attribute_names.collect { |name| [name.to_s, attr_json_registry.type_for_attribute(name)]}.to_h
|
146
|
+
end
|
147
|
+
|
148
|
+
|
68
149
|
# Type can be an instance of an ActiveModel::Type::Value subclass, or a symbol that will
|
69
150
|
# be looked up in `ActiveModel::Type.lookup`
|
70
151
|
#
|
@@ -107,27 +188,6 @@ module AttrJson
|
|
107
188
|
end
|
108
189
|
end
|
109
190
|
|
110
|
-
# This should kind of be considered 'protected', but the semantics
|
111
|
-
# of how we want to call it don't give us a visibility modifier that works.
|
112
|
-
# Prob means refactoring called for. TODO?
|
113
|
-
def fill_in_defaults(hash)
|
114
|
-
# Only if we need to mutate it to add defaults, we'll dup it first. deep_dup not neccesary
|
115
|
-
# since we're only modifying top-level here.
|
116
|
-
duped = false
|
117
|
-
attr_json_registry.definitions.each do |definition|
|
118
|
-
if definition.has_default? && ! (hash.has_key?(definition.store_key.to_s) || hash.has_key?(definition.store_key.to_sym))
|
119
|
-
unless duped
|
120
|
-
hash = hash.dup
|
121
|
-
duped = true
|
122
|
-
end
|
123
|
-
|
124
|
-
hash[definition.store_key] = definition.provide_default!
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
hash
|
129
|
-
end
|
130
|
-
|
131
191
|
private
|
132
192
|
|
133
193
|
# Define an anonymous module and include it, so can still be easily
|
@@ -143,11 +203,15 @@ module AttrJson
|
|
143
203
|
end
|
144
204
|
|
145
205
|
def initialize(attributes = {})
|
146
|
-
|
147
|
-
|
148
|
-
|
206
|
+
super
|
207
|
+
|
208
|
+
fill_in_defaults!
|
209
|
+
end
|
149
210
|
|
150
|
-
|
211
|
+
# inspired by https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/activemodel/lib/active_model/attributes.rb
|
212
|
+
def initialize_dup(other) # :nodoc:
|
213
|
+
@attributes = @attributes.deep_dup
|
214
|
+
super
|
151
215
|
end
|
152
216
|
|
153
217
|
def attributes
|
@@ -185,6 +249,11 @@ module AttrJson
|
|
185
249
|
self.class.attr_json_registry.has_attribute?(str)
|
186
250
|
end
|
187
251
|
|
252
|
+
# like the ActiveModel::Attributes method
|
253
|
+
def attribute_names
|
254
|
+
self.class.attribute_names
|
255
|
+
end
|
256
|
+
|
188
257
|
# Override from ActiveModel::Serialization to #serialize
|
189
258
|
# by type to make sure any values set directly on hash still
|
190
259
|
# get properly type-serialized.
|
@@ -217,12 +286,9 @@ module AttrJson
|
|
217
286
|
end
|
218
287
|
|
219
288
|
# Two AttrJson::Model objects are equal if they are the same class
|
220
|
-
#
|
221
|
-
# TODO: Should we allow subclasses to be equal, or should they have to be the
|
222
|
-
# exact same class?
|
289
|
+
# AND their #attributes are equal.
|
223
290
|
def ==(other_object)
|
224
|
-
|
225
|
-
other_object.attributes == self.attributes
|
291
|
+
other_object.class == self.class && other_object.attributes == self.attributes
|
226
292
|
end
|
227
293
|
|
228
294
|
# ActiveRecord objects [have a](https://github.com/rails/rails/blob/v5.1.5/activerecord/lib/active_record/nested_attributes.rb#L367-L374)
|
@@ -234,8 +300,25 @@ module AttrJson
|
|
234
300
|
false
|
235
301
|
end
|
236
302
|
|
303
|
+
# like ActiveModel::Attributes at
|
304
|
+
# https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/activemodel/lib/active_model/attributes.rb#L120
|
305
|
+
#
|
306
|
+
# is not a full deep freeze
|
307
|
+
def freeze
|
308
|
+
attributes.freeze unless frozen?
|
309
|
+
super
|
310
|
+
end
|
311
|
+
|
237
312
|
private
|
238
313
|
|
314
|
+
def fill_in_defaults!
|
315
|
+
self.class.attr_json_registry.definitions.each do |definition|
|
316
|
+
if definition.has_default? && !attributes.has_key?(definition.name.to_s)
|
317
|
+
self.send("#{definition.name.to_s}=", definition.provide_default!)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
239
322
|
def _attr_json_write(key, value)
|
240
323
|
if attribute_def = self.class.attr_json_registry[key.to_sym]
|
241
324
|
attributes[key.to_s] = attribute_def.cast(value)
|
@@ -247,7 +330,7 @@ module AttrJson
|
|
247
330
|
|
248
331
|
|
249
332
|
def _attr_json_write_unknown_attribute(key, value)
|
250
|
-
case
|
333
|
+
case self.class.attr_json_config.unknown_key
|
251
334
|
when :strip
|
252
335
|
# drop it, no-op
|
253
336
|
when :allow
|
@@ -16,7 +16,11 @@ module AttrJson
|
|
16
16
|
|
17
17
|
def assign_nested_attributes(attributes)
|
18
18
|
if attr_def.array_type?
|
19
|
-
|
19
|
+
if attr_def.type.base_type_primitive?
|
20
|
+
assign_nested_attributes_for_primitive_array(attributes)
|
21
|
+
else
|
22
|
+
assign_nested_attributes_for_model_array(attributes)
|
23
|
+
end
|
20
24
|
else
|
21
25
|
assign_nested_attributes_for_single_model(attributes)
|
22
26
|
end
|
@@ -38,6 +42,27 @@ module AttrJson
|
|
38
42
|
end
|
39
43
|
end
|
40
44
|
|
45
|
+
|
46
|
+
# Implementation for an `#{attribute_name}_attributes=` method, when the attr_json
|
47
|
+
# attribute in question is recognized as an array of primitive values (not nested models)
|
48
|
+
#
|
49
|
+
# Really just exists to filter out blank/empty strings with reject_if.
|
50
|
+
#
|
51
|
+
# It will insist on filtering out empty strings and nils from arrays (ignores reject_if argument),
|
52
|
+
# since that's the only reason to use it. It will respect limit argument.
|
53
|
+
#
|
54
|
+
# Filtering out empty strings can be convenient for using a hidden field in a form to
|
55
|
+
# make sure an empty array gets set if all individual fields are removed from form using
|
56
|
+
# cocoon-like javascript.
|
57
|
+
def assign_nested_attributes_for_primitive_array(attributes_array)
|
58
|
+
options = nested_attributes_options[attr_name]
|
59
|
+
check_record_limit!(options[:limit], attributes_array)
|
60
|
+
|
61
|
+
attributes_array = attributes_array.reject { |value| value.blank? }
|
62
|
+
|
63
|
+
model_send("#{attr_name}=", attributes_array)
|
64
|
+
end
|
65
|
+
|
41
66
|
# Copied with signficant modifications from:
|
42
67
|
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/nested_attributes.rb#L407
|
43
68
|
def assign_nested_attributes_for_single_model(attributes)
|
@@ -29,6 +29,11 @@ module AttrJson
|
|
29
29
|
# * _allow_destroy_, no such option. Effectively always true, doesn't make sense to try to gate this with our implementation.
|
30
30
|
# * _update_only_, no such option. Not really relevant to this architecture where you're embedded models have no independent existence.
|
31
31
|
#
|
32
|
+
# If called on an array of 'primitive' (not AttrJson::Model) objects, it will do a kind of weird
|
33
|
+
# thing where it creates an `#{attribute_name}_attributes=` method that does nothing
|
34
|
+
# but filter out empty strings and nil values. This can be convenient in hackily form
|
35
|
+
# handling array of primitives, see guide doc on forms.
|
36
|
+
#
|
32
37
|
# @overload attr_json_accepts_nested_attributes_for(define_build_method: true, reject_if: nil, limit: nil)
|
33
38
|
# @param define_build_method [Boolean] Default true, provide `build_attribute_name`
|
34
39
|
# method that works like you expect. [Cocoon](https://github.com/nathanvda/cocoon),
|
@@ -74,7 +79,8 @@ module AttrJson
|
|
74
79
|
end
|
75
80
|
end
|
76
81
|
|
77
|
-
|
82
|
+
# No build method for our wacky array of primitive type.
|
83
|
+
if options[:define_build_method] && !(attr_def.array_type? && attr_def.type.base_type_primitive?)
|
78
84
|
_attr_jsons_module.module_eval do
|
79
85
|
build_method_name = "build_#{attr_name.to_s.singularize}"
|
80
86
|
if method_defined?(build_method_name)
|
@@ -137,20 +137,20 @@ module AttrJson
|
|
137
137
|
end
|
138
138
|
|
139
139
|
def saved_changes
|
140
|
-
|
141
|
-
return {} if
|
140
|
+
original_saved_changes = model.saved_changes
|
141
|
+
return {} if original_saved_changes.blank?
|
142
142
|
|
143
143
|
json_attr_changes = registry.definitions.collect do |definition|
|
144
|
-
if container_change =
|
145
|
-
old_v = container_change
|
146
|
-
new_v = container_change
|
144
|
+
if container_change = original_saved_changes[definition.container_attribute]
|
145
|
+
old_v = container_change.dig(0, definition.store_key)
|
146
|
+
new_v = container_change.dig(1, definition.store_key)
|
147
147
|
if old_v != new_v
|
148
148
|
[ definition.name.to_s, formatted_before_after(old_v, new_v, definition) ]
|
149
149
|
end
|
150
150
|
end
|
151
151
|
end.compact.to_h
|
152
152
|
|
153
|
-
prepared_changes(json_attr_changes,
|
153
|
+
prepared_changes(json_attr_changes, original_saved_changes)
|
154
154
|
end
|
155
155
|
|
156
156
|
def saved_changes?
|
@@ -198,21 +198,21 @@ module AttrJson
|
|
198
198
|
end
|
199
199
|
|
200
200
|
def changes_to_save
|
201
|
-
|
201
|
+
original_changes_to_save = model.changes_to_save
|
202
202
|
|
203
|
-
return {} if
|
203
|
+
return {} if original_changes_to_save.blank?
|
204
204
|
|
205
205
|
json_attr_changes = registry.definitions.collect do |definition|
|
206
|
-
if container_change =
|
207
|
-
old_v = container_change
|
208
|
-
new_v = container_change
|
206
|
+
if container_change = original_changes_to_save[definition.container_attribute]
|
207
|
+
old_v = container_change.dig(0, definition.store_key)
|
208
|
+
new_v = container_change.dig(1, definition.store_key)
|
209
209
|
if old_v != new_v
|
210
210
|
[ definition.name.to_s, formatted_before_after(old_v, new_v, definition) ]
|
211
211
|
end
|
212
212
|
end
|
213
213
|
end.compact.to_h
|
214
214
|
|
215
|
-
prepared_changes(json_attr_changes,
|
215
|
+
prepared_changes(json_attr_changes, original_changes_to_save)
|
216
216
|
end
|
217
217
|
|
218
218
|
def has_changes_to_save?
|
@@ -270,7 +270,13 @@ module AttrJson
|
|
270
270
|
# find it from currently declared attributes.
|
271
271
|
# https://github.com/rails/rails/blob/6aa5cf03ea8232180ffbbae4c130b051f813c670/activemodel/lib/active_model/attribute_methods.rb#L463-L468
|
272
272
|
def matched_attribute_method(method_name)
|
273
|
-
|
273
|
+
if self.class.respond_to?(:attribute_method_patterns_matching, true)
|
274
|
+
# Rails 7.1+
|
275
|
+
matches = self.class.send(:attribute_method_patterns_matching, method_name)
|
276
|
+
else
|
277
|
+
matches = self.class.send(:attribute_method_matchers_matching, method_name)
|
278
|
+
end
|
279
|
+
|
274
280
|
matches.detect do |match|
|
275
281
|
registry.has_attribute?(match.attr_name)
|
276
282
|
end
|
@@ -10,6 +10,20 @@ module AttrJson
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def contains_relation
|
13
|
+
contains_relation_impl do |relation, query, params|
|
14
|
+
relation.where(query, params)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def contains_not_relation
|
19
|
+
contains_relation_impl do |relation, query, params|
|
20
|
+
relation.where.not(query, params)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def contains_relation_impl
|
13
27
|
result_relation = relation
|
14
28
|
|
15
29
|
group_attributes_by_container.each do |container_attribute, attributes|
|
@@ -18,14 +32,12 @@ module AttrJson
|
|
18
32
|
attributes.each do |key, value|
|
19
33
|
add_to_param_hash!(param_hash, key, value)
|
20
34
|
end
|
21
|
-
result_relation = result_relation
|
35
|
+
result_relation = yield(result_relation, "#{relation.table_name}.#{container_attribute} @> (?)::jsonb", param_hash.to_json)
|
22
36
|
end
|
23
37
|
|
24
38
|
result_relation
|
25
39
|
end
|
26
40
|
|
27
|
-
protected
|
28
|
-
|
29
41
|
def merge_param_hash!(original, new)
|
30
42
|
original.deep_merge!(new) do |key, old_val, new_val|
|
31
43
|
if old_val.is_a?(Array) && old_val.first.is_a?(Hash) && new_val.is_a?(Array) && new_val.first.is_a?(Hash)
|
@@ -17,6 +17,8 @@ module AttrJson
|
|
17
17
|
#
|
18
18
|
# some_model.jsonb_contains(a_string: "foo").first
|
19
19
|
#
|
20
|
+
# some_model.not_jsonb_contains(a_string: "bar").first
|
21
|
+
#
|
20
22
|
# See more in {file:README} docs.
|
21
23
|
module QueryScopes
|
22
24
|
extend ActiveSupport::Concern
|
@@ -29,6 +31,10 @@ module AttrJson
|
|
29
31
|
scope(:jsonb_contains, lambda do |attributes|
|
30
32
|
QueryBuilder.new(self, attributes).contains_relation
|
31
33
|
end)
|
34
|
+
|
35
|
+
scope(:not_jsonb_contains, lambda do |attributes|
|
36
|
+
QueryBuilder.new(self, attributes).contains_not_relation
|
37
|
+
end)
|
32
38
|
end
|
33
39
|
end
|
34
40
|
end
|