attr_json 0.2.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|