attr_json 1.4.0 → 2.6.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 +4 -4
- data/.github/workflows/ci.yml +22 -22
- data/.github/workflows/future_rails_ci.yml +3 -3
- data/Appraisals +40 -29
- data/CHANGELOG.md +144 -1
- data/Gemfile +8 -2
- data/README.md +83 -63
- data/attr_json.gemspec +8 -7
- data/doc_src/forms.md +3 -14
- data/gemfiles/rails_6_0.gemfile +3 -2
- data/gemfiles/rails_6_1.gemfile +2 -2
- data/gemfiles/rails_7_0.gemfile +2 -3
- data/gemfiles/{rails_5_1.gemfile → rails_7_1.gemfile} +4 -4
- data/gemfiles/{rails_5_2.gemfile → rails_7_2.gemfile} +4 -4
- data/gemfiles/{rails_5_0.gemfile → rails_8_0.gemfile} +5 -6
- data/gemfiles/rails_8_1.gemfile +18 -0
- data/gemfiles/rails_edge.gemfile +4 -4
- data/lib/attr_json/attribute_definition/registry.rb +43 -9
- data/lib/attr_json/attribute_definition.rb +51 -14
- data/lib/attr_json/config.rb +1 -2
- data/lib/attr_json/model/nested_model_validator.rb +27 -0
- data/lib/attr_json/model.rb +185 -53
- data/lib/attr_json/nested_attributes/builder.rb +2 -0
- data/lib/attr_json/nested_attributes/multiparameter_attribute_writer.rb +2 -0
- data/lib/attr_json/nested_attributes/writer.rb +6 -6
- data/lib/attr_json/nested_attributes.rb +7 -1
- data/lib/attr_json/record/query_builder.rb +2 -0
- data/lib/attr_json/record/query_scopes.rb +2 -0
- data/lib/attr_json/record.rb +113 -88
- data/lib/attr_json/serialization_coder_from_type.rb +2 -0
- data/lib/attr_json/type/array.rb +12 -3
- data/lib/attr_json/type/container_attribute.rb +2 -0
- data/lib/attr_json/type/model.rb +15 -4
- data/lib/attr_json/type/polymorphic_model.rb +43 -23
- data/lib/attr_json/version.rb +1 -1
- data/lib/attr_json.rb +21 -6
- data/playground_models.rb +2 -2
- metadata +15 -32
- data/doc_src/dirty_tracking.md +0 -155
- data/lib/attr_json/record/dirty.rb +0 -281
data/lib/attr_json/record.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'attr_json/attribute_definition'
|
|
2
4
|
require 'attr_json/attribute_definition/registry'
|
|
3
5
|
require 'attr_json/type/container_attribute'
|
|
@@ -24,35 +26,61 @@ module AttrJson
|
|
|
24
26
|
|
|
25
27
|
class_attribute :attr_json_registry, instance_accessor: false
|
|
26
28
|
self.attr_json_registry = AttrJson::AttributeDefinition::Registry.new
|
|
27
|
-
end
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
# Ensure that rails attributes tracker knows about values we just fetched
|
|
31
|
+
after_initialize do
|
|
32
|
+
attr_json_sync_to_rails_attributes
|
|
33
|
+
end
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
# After a safe, rails attribute dirty tracking ends up re-creating
|
|
36
|
+
# new objects for attribute values, so we need to sync again
|
|
37
|
+
# so mutation effects both.
|
|
38
|
+
after_save do
|
|
39
|
+
attr_json_sync_to_rails_attributes
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Sync all values FROM the json_attributes json column TO rails attributes
|
|
33
44
|
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
# well as `self.class.columns_hash[attr_name]` which you definitely can not (which is probably not bad),
|
|
37
|
-
# and has no way to use the value-translation semantics independently of that. May be a problem if
|
|
38
|
-
# ActiveRecord changes it's query method semantics in the future, will have to be sync'd here.
|
|
45
|
+
# If values have for some reason gotten out of sync this will make them the
|
|
46
|
+
# identical objects again, with the container hash value being the source.
|
|
39
47
|
#
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
# In some cases, the values may already be equivalent but different objects --
|
|
49
|
+
# This is meant to ensure they are the _same object_ in both places, so
|
|
50
|
+
# mutation of mutable object will effect both places, for instance for dirty
|
|
51
|
+
# tracking.
|
|
52
|
+
def attr_json_sync_to_rails_attributes
|
|
53
|
+
self.class.attr_json_registry.definitions.group_by(&:container_attribute).each_pair do |container_attribute, definitions|
|
|
54
|
+
begin
|
|
55
|
+
# column may have eg been left out of an explicit 'select'
|
|
56
|
+
next unless has_attribute?(container_attribute)
|
|
57
|
+
|
|
58
|
+
container_value = public_send(container_attribute)
|
|
59
|
+
|
|
60
|
+
# isn't expected to be possible to be nil rather than empty hash, but
|
|
61
|
+
# if it is from some edge case, well, we don't have values to sync, fine
|
|
62
|
+
next if container_value.nil?
|
|
63
|
+
|
|
64
|
+
definitions.each do |attribute_def|
|
|
65
|
+
attr_name = attribute_def.name
|
|
66
|
+
value = container_value[attribute_def.store_key]
|
|
67
|
+
|
|
68
|
+
unless value.nil?
|
|
69
|
+
# TODO, can we just make this use the setter?
|
|
70
|
+
write_attribute(attr_name, value)
|
|
71
|
+
|
|
72
|
+
clear_attribute_change(attr_name) if persisted?
|
|
73
|
+
|
|
74
|
+
# writing and clearning will result in a new object stored in
|
|
75
|
+
# rails attributes, we want
|
|
76
|
+
# to make sure the exact same object is in the json attribute,
|
|
77
|
+
# so in-place mutation changes to it are reflected in both places.
|
|
78
|
+
container_value[attribute_def.store_key] = read_attribute(attr_name)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
rescue AttrJson::Type::Model::BadCast, AttrJson::Type::PolymorphicModel::TypeError => e
|
|
82
|
+
# There was bad data in the DB, we're just going to skip the Rails attribute sync.
|
|
83
|
+
# Should we log?
|
|
56
84
|
end
|
|
57
85
|
end
|
|
58
86
|
end
|
|
@@ -93,7 +121,8 @@ module AttrJson
|
|
|
93
121
|
end
|
|
94
122
|
|
|
95
123
|
|
|
96
|
-
|
|
124
|
+
# Registers an attr_json attribute, and a Rails attribute covering it.
|
|
125
|
+
#
|
|
97
126
|
# Type can be a symbol that will be looked up in `ActiveModel::Type.lookup`,
|
|
98
127
|
# or an ActiveModel:::Type::Value).
|
|
99
128
|
#
|
|
@@ -102,6 +131,8 @@ module AttrJson
|
|
|
102
131
|
# @param type [ActiveModel::Type::Value] An instance of an ActiveModel::Type::Value (or subclass)
|
|
103
132
|
#
|
|
104
133
|
# @option options [Boolean] :array (false) Make this attribute an array of given type.
|
|
134
|
+
# Array types default to an empty array. If you want to turn that off, you can add
|
|
135
|
+
# `default: AttrJson::AttributeDefinition::NO_DEFAULT_PROVIDED`
|
|
105
136
|
#
|
|
106
137
|
# @option options [Object] :default (nil) Default value, if a Proc object it will be #call'd
|
|
107
138
|
# for default.
|
|
@@ -113,100 +144,94 @@ module AttrJson
|
|
|
113
144
|
# json(b) ActiveRecord attribute/column to serialize as a key in. Defaults to
|
|
114
145
|
# `attr_json_config.default_container_attribute`, which defaults to `:json_attributes`
|
|
115
146
|
#
|
|
116
|
-
# @option options [Boolean] :validate (true)
|
|
117
|
-
#
|
|
147
|
+
# @option options [Boolean] :validate (true) validation errors on nested models in the attributes
|
|
148
|
+
# should post up to self similar to Rails ActiveRecord::Validations::AssociatedValidator on
|
|
149
|
+
# associated objects.
|
|
150
|
+
#
|
|
151
|
+
# @option options [Boolean,Hash] :accepts_nested_attributes. If true, equivalent
|
|
152
|
+
# of writing `attr_json_accepts_nested_attributes :attribute_name`. If value is a hash,
|
|
153
|
+
# then same, but with hash as options to `attr_json_accepts_nested_attributes`.
|
|
154
|
+
# Default taken from `attr_json_config.default_accepts_nested_attributes`, for
|
|
155
|
+
# array or model types where it is applicable.
|
|
118
156
|
#
|
|
119
|
-
# @option options [Boolean] :rails_attribute (false) Create an actual ActiveRecord
|
|
120
|
-
# `attribute` for name param. A Rails attribute isn't needed for our functionality,
|
|
121
|
-
# but registering thusly will let the type be picked up by simple_form and
|
|
122
|
-
# other tools that may look for it via Rails attribute APIs. Default can be changed
|
|
123
|
-
# with `attr_json_config(default_rails_attribute: true)`
|
|
124
157
|
def attr_json(name, type, **options)
|
|
125
158
|
options = {
|
|
126
|
-
rails_attribute: self.attr_json_config.default_rails_attribute,
|
|
127
159
|
validate: true,
|
|
128
160
|
container_attribute: self.attr_json_config.default_container_attribute,
|
|
129
|
-
accepts_nested_attributes: self.attr_json_config.default_accepts_nested_attributes
|
|
130
161
|
}.merge!(options)
|
|
131
|
-
options.assert_valid_keys(AttributeDefinition::VALID_OPTIONS + [:validate, :
|
|
162
|
+
options.assert_valid_keys(AttributeDefinition::VALID_OPTIONS + [:validate, :accepts_nested_attributes])
|
|
132
163
|
container_attribute = options[:container_attribute]
|
|
133
164
|
|
|
134
|
-
# TODO arg check container_attribute make sure it exists. Hard cause
|
|
135
|
-
# schema isn't loaded yet when class def is loaded. Maybe not.
|
|
136
165
|
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
|
|
142
|
-
# We set default to empty hash, because that 'tricks' AR into knowing any
|
|
143
|
-
# application of defaults is a change that needs to be saved.
|
|
144
|
-
unless attributes_to_define_after_schema_loads[container_attribute.to_s] &&
|
|
145
|
-
attributes_to_define_after_schema_loads[container_attribute.to_s].first.is_a?(AttrJson::Type::ContainerAttribute) &&
|
|
146
|
-
attributes_to_define_after_schema_loads[container_attribute.to_s].first.model == self
|
|
147
|
-
# If this is already defined, but was for superclass, we need to define it again for
|
|
148
|
-
# this class.
|
|
166
|
+
# Make sure to "lazily" register attribute for *container* class if this is the first time
|
|
167
|
+
# this container attribute hsa been encountered for this specific class. The registry
|
|
168
|
+
# helps us keep track. Kinda messy, in future we may want a more explicit API
|
|
169
|
+
# that does not require us to implicitly track first-time per-container.
|
|
170
|
+
unless self.attr_json_registry.container_attribute_registered?(model: self, attribute_name: container_attribute.to_sym)
|
|
149
171
|
attribute container_attribute.to_sym, AttrJson::Type::ContainerAttribute.new(self, container_attribute), default: -> { {} }
|
|
172
|
+
self.attr_json_registry.register_container_attribute(model: self, attribute_name: container_attribute.to_sym)
|
|
150
173
|
end
|
|
151
174
|
|
|
152
175
|
self.attr_json_registry = attr_json_registry.with(
|
|
153
|
-
AttributeDefinition.new(name.to_sym, type, options.except(:
|
|
176
|
+
AttributeDefinition.new(name.to_sym, type, options.except(:validate, :accepts_nested_attributes))
|
|
154
177
|
)
|
|
155
178
|
|
|
156
|
-
# By default, automatically validate nested models
|
|
179
|
+
# By default, automatically validate nested models, allowing nils.
|
|
157
180
|
if type.kind_of?(AttrJson::Type::Model) && options[:validate]
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
attribute_args = attr_json_definition.has_default? ? { default: attr_json_definition.default_argument } : {}
|
|
167
|
-
self.attribute name.to_sym, attr_json_definition.type, **attribute_args
|
|
168
|
-
|
|
169
|
-
# Ensure that rails attributes tracker knows about value we just fetched
|
|
170
|
-
# for this particular attribute. Yes, we are registering an after_find for each
|
|
171
|
-
# attr_json registered with rails_attribute:true, using the `name` from above under closure. .
|
|
172
|
-
after_find do
|
|
173
|
-
value = public_send(name)
|
|
174
|
-
if value && has_attribute?(name.to_sym)
|
|
175
|
-
write_attribute(name.to_sym, value)
|
|
176
|
-
self.send(:clear_attribute_changes, [name.to_sym])
|
|
181
|
+
# implementation adopted from:
|
|
182
|
+
# https://github.com/rails/rails/blob/v7.0.4.1/activerecord/lib/active_record/validations/associated.rb#L6-L10
|
|
183
|
+
#
|
|
184
|
+
# but had to customize to allow nils in an array through
|
|
185
|
+
validates_each name.to_sym do |record, attr, value|
|
|
186
|
+
if Array(value).reject { |element| element.nil? || element.valid? }.any?
|
|
187
|
+
record.errors.add(attr, :invalid, value: value)
|
|
177
188
|
end
|
|
178
189
|
end
|
|
179
190
|
end
|
|
180
191
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
#
|
|
186
|
-
# But in fact just getting/setting in the hash provided to us by ActiveRecord json type
|
|
187
|
-
# container works BETTER for dirty tracking. We had a test that only passed doing it
|
|
188
|
-
# this simple way.
|
|
192
|
+
# Register as a Rails attribute
|
|
193
|
+
attr_json_definition = attr_json_registry[name]
|
|
194
|
+
attribute_args = attr_json_definition.has_default? ? { default: attr_json_definition.default_argument } : {}
|
|
195
|
+
self.attribute name.to_sym, attr_json_definition.type, **attribute_args
|
|
189
196
|
|
|
197
|
+
# For getter and setter, we consider the container has the "canonical" data location.
|
|
198
|
+
# But setter also writes to rails attribute, and tries to keep them in sync with the
|
|
199
|
+
# *same object*, so mutations happen to both places.
|
|
200
|
+
#
|
|
201
|
+
# This began roughly modelled on approach of Rail sstore_accessor implementation:
|
|
202
|
+
# https://github.com/rails/rails/blob/74c3e43fba458b9b863d27f0c45fd2d8dc603cbc/activerecord/lib/active_record/store.rb#L90-L96
|
|
203
|
+
#
|
|
204
|
+
# ...But wound up with lots of evolution to try to get dirty tracking working as well
|
|
205
|
+
# as we could -- without a completely custom separate dirty tracking implementation
|
|
206
|
+
# like store_accessor tries!
|
|
207
|
+
_attr_jsons_module.module_eval do
|
|
190
208
|
define_method("#{name}=") do |value|
|
|
191
|
-
super(value)
|
|
209
|
+
super(value) # should write to rails attribute
|
|
210
|
+
|
|
211
|
+
# write to container hash, with value read from attribute to try to keep objects
|
|
212
|
+
# sync'd to exact same object in rails attribute and container hash.
|
|
192
213
|
attribute_def = self.class.attr_json_registry.fetch(name.to_sym)
|
|
193
|
-
public_send(attribute_def.container_attribute)[attribute_def.store_key] =
|
|
214
|
+
public_send(attribute_def.container_attribute)[attribute_def.store_key] = read_attribute(name)
|
|
194
215
|
end
|
|
195
216
|
|
|
196
217
|
define_method("#{name}") do
|
|
218
|
+
# read from container hash -- we consider that the canonical location.
|
|
197
219
|
attribute_def = self.class.attr_json_registry.fetch(name.to_sym)
|
|
198
220
|
public_send(attribute_def.container_attribute)[attribute_def.store_key]
|
|
199
221
|
end
|
|
222
|
+
end
|
|
200
223
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
224
|
+
accepts_nested_attributes = if options.has_key?(:accepts_nested_attributes)
|
|
225
|
+
options[:accepts_nested_attributes]
|
|
226
|
+
elsif attr_json_definition.single_model_type? || attr_json_definition.array_type?
|
|
227
|
+
# use configured default only if we have a type appropriate for it!
|
|
228
|
+
self.attr_json_config.default_accepts_nested_attributes
|
|
229
|
+
else
|
|
230
|
+
false
|
|
205
231
|
end
|
|
206
232
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
options = options[:accepts_nested_attributes] == true ? {} : options[:accepts_nested_attributes]
|
|
233
|
+
if accepts_nested_attributes
|
|
234
|
+
options = accepts_nested_attributes == true ? {} : accepts_nested_attributes
|
|
210
235
|
self.attr_json_accepts_nested_attributes_for name, **options
|
|
211
236
|
end
|
|
212
237
|
end
|
data/lib/attr_json/type/array.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module AttrJson
|
|
2
4
|
module Type
|
|
3
5
|
# You can wrap any ActiveModel::Type in one of these, and it's magically
|
|
@@ -30,6 +32,10 @@ module AttrJson
|
|
|
30
32
|
convert_to_array(value).collect { |v| base_type.deserialize(v) }
|
|
31
33
|
end
|
|
32
34
|
|
|
35
|
+
def changed_in_place?(raw_old_value, new_value)
|
|
36
|
+
serialize(new_value) != raw_old_value
|
|
37
|
+
end
|
|
38
|
+
|
|
33
39
|
# This is used only by our own keypath-chaining query stuff.
|
|
34
40
|
def value_for_contains_query(key_path_arr, value)
|
|
35
41
|
[
|
|
@@ -41,10 +47,13 @@ module AttrJson
|
|
|
41
47
|
]
|
|
42
48
|
end
|
|
43
49
|
|
|
44
|
-
#
|
|
45
|
-
#
|
|
50
|
+
# Soft-deprecated. You probably want to use
|
|
51
|
+
#
|
|
52
|
+
# AttrJson::AttributeDefinition#array_of_primitive_type?
|
|
53
|
+
#
|
|
54
|
+
# instead where possible.
|
|
46
55
|
def base_type_primitive?
|
|
47
|
-
!
|
|
56
|
+
! AttrJson::AttributeDefinition.single_model_type?(base_type)
|
|
48
57
|
end
|
|
49
58
|
|
|
50
59
|
protected
|
data/lib/attr_json/type/model.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module AttrJson
|
|
2
4
|
module Type
|
|
3
5
|
# An ActiveModel::Type representing a particular AttrJson::Model
|
|
@@ -8,13 +10,22 @@ module AttrJson
|
|
|
8
10
|
# but normally that's only done in AttrJson::Model.to_type, there isn't
|
|
9
11
|
# an anticipated need to create from any other place.
|
|
10
12
|
#
|
|
13
|
+
#
|
|
11
14
|
class Model < ::ActiveModel::Type::Value
|
|
12
15
|
class BadCast < ArgumentError ; end
|
|
13
16
|
|
|
14
|
-
attr_accessor :model
|
|
15
|
-
|
|
17
|
+
attr_accessor :model, :strip_nils
|
|
18
|
+
|
|
19
|
+
# @param model [AttrJson::Model] the model _class_ object
|
|
20
|
+
# @param strip_nils [Symbol,Boolean] [true, false, or :safely]
|
|
21
|
+
# (default :safely), As a type, should we strip nils when serialiing?
|
|
22
|
+
# This value passed to AttrJson::Model#serialized_hash(strip_nils).
|
|
23
|
+
# by default it's :safely, we strip nils when it can be done safely
|
|
24
|
+
# to preserve default overrides.
|
|
25
|
+
def initialize(model, strip_nils: :safely)
|
|
16
26
|
#TODO type check, it really better be a AttrJson::Model. maybe?
|
|
17
27
|
@model = model
|
|
28
|
+
@strip_nils = strip_nils
|
|
18
29
|
end
|
|
19
30
|
|
|
20
31
|
def type
|
|
@@ -51,9 +62,9 @@ module AttrJson
|
|
|
51
62
|
if v.nil?
|
|
52
63
|
nil
|
|
53
64
|
elsif v.kind_of?(model)
|
|
54
|
-
v.serializable_hash
|
|
65
|
+
v.serializable_hash(strip_nils: strip_nils)
|
|
55
66
|
else
|
|
56
|
-
(cast_v = cast(v)) && cast_v.serializable_hash
|
|
67
|
+
(cast_v = cast(v)) && cast_v.serializable_hash(strip_nils: strip_nils)
|
|
57
68
|
end
|
|
58
69
|
end
|
|
59
70
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module AttrJson
|
|
2
4
|
module Type
|
|
3
5
|
# AttrJson::Type::PolymorphicModel can be used to create attr_json attributes
|
|
@@ -100,17 +102,11 @@ module AttrJson
|
|
|
100
102
|
end
|
|
101
103
|
|
|
102
104
|
def cast(v)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
cast_from_hash(v.to_hash)
|
|
109
|
-
elsif v.respond_to?(:to_h)
|
|
110
|
-
cast_from_hash(v.to_h)
|
|
111
|
-
else
|
|
112
|
-
raise_bad_model_name(v.class, v)
|
|
113
|
-
end
|
|
105
|
+
cast_or_deserialize(v, :cast)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def deserialize(v)
|
|
109
|
+
cast_or_deserialize(v, :deserialize)
|
|
114
110
|
end
|
|
115
111
|
|
|
116
112
|
def serialize(v)
|
|
@@ -127,10 +123,6 @@ module AttrJson
|
|
|
127
123
|
type.serialize(v).merge(type_key => model_name)
|
|
128
124
|
end
|
|
129
125
|
|
|
130
|
-
def deserialize(v)
|
|
131
|
-
cast(v)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
126
|
def type_for_model_name(model_name)
|
|
135
127
|
model_type_lookup[model_name]
|
|
136
128
|
end
|
|
@@ -153,15 +145,29 @@ module AttrJson
|
|
|
153
145
|
|
|
154
146
|
protected
|
|
155
147
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
148
|
+
# We need to make sure to call the correct operation on
|
|
149
|
+
# the model type, so that we get the same result as if
|
|
150
|
+
# we had called the type directly
|
|
151
|
+
#
|
|
152
|
+
# @param v [Object, nil] the value to cast or deserialize
|
|
153
|
+
# @param operation [Symbol] :cast or :deserialize
|
|
154
|
+
def cast_or_deserialize(v, operation)
|
|
155
|
+
if v.nil?
|
|
156
|
+
v
|
|
157
|
+
elsif model_names.include?(v.class.name)
|
|
158
|
+
v
|
|
159
|
+
elsif v.respond_to?(:to_hash)
|
|
160
|
+
model_from_hash(v.to_hash, operation)
|
|
161
|
+
elsif v.respond_to?(:to_h)
|
|
162
|
+
model_from_hash(v.to_h, operation)
|
|
163
|
+
else
|
|
164
|
+
raise_bad_model_name(v.class, v)
|
|
165
|
+
end
|
|
162
166
|
end
|
|
163
167
|
|
|
164
|
-
|
|
168
|
+
# @param hash [Hash] the value to cast or deserialize
|
|
169
|
+
# @param operation [Symbol] :cast or :deserialize
|
|
170
|
+
def model_from_hash(hash, operation)
|
|
165
171
|
new_hash = hash.stringify_keys
|
|
166
172
|
model_name = new_hash.delete(type_key.to_s)
|
|
167
173
|
|
|
@@ -171,7 +177,21 @@ module AttrJson
|
|
|
171
177
|
|
|
172
178
|
raise_bad_model_name(model_name, hash) if type.nil?
|
|
173
179
|
|
|
174
|
-
|
|
180
|
+
if operation == :deserialize
|
|
181
|
+
type.deserialize(new_hash)
|
|
182
|
+
elsif operation == :cast
|
|
183
|
+
type.cast(new_hash)
|
|
184
|
+
else
|
|
185
|
+
raise ArgumentError, "Unknown operation #{operation}"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def raise_missing_type_key(value)
|
|
190
|
+
raise TypeError, "AttrJson::Type::Polymorphic can't cast without '#{type_key}' key: #{value}"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def raise_bad_model_name(name, value)
|
|
194
|
+
raise TypeError, "This AttrJson::Type::PolymorphicType can only include {#{model_names.join(', ')}}, not '#{name}': #{value.inspect}"
|
|
175
195
|
end
|
|
176
196
|
end
|
|
177
197
|
end
|
data/lib/attr_json/version.rb
CHANGED
data/lib/attr_json.rb
CHANGED
|
@@ -9,11 +9,26 @@ require 'attr_json/nested_attributes'
|
|
|
9
9
|
require 'attr_json/record/query_scopes'
|
|
10
10
|
require 'attr_json/type/polymorphic_model'
|
|
11
11
|
|
|
12
|
-
# Dirty not supported on Rails 5.0
|
|
13
|
-
if Gem.loaded_specs["activerecord"].version.release >= Gem::Version.new('5.1')
|
|
14
|
-
require 'attr_json/record/dirty'
|
|
15
|
-
end
|
|
16
|
-
|
|
17
12
|
module AttrJson
|
|
18
|
-
|
|
13
|
+
# We need to convert Symbols to strings a lot at present -- ActiveRecord does too, so
|
|
14
|
+
# not too suprrising.
|
|
15
|
+
#
|
|
16
|
+
# In Rails 3.0 and above, we can use Symbol#name to get a frozen string back
|
|
17
|
+
# and avoid extra allocations. https://bugs.ruby-lang.org/issues/16150
|
|
18
|
+
#
|
|
19
|
+
# Ruby 2.7 doens't yet have it though. As long as we are supporting ruby 2.7,
|
|
20
|
+
# we'll just check at runtime to keep this lean
|
|
21
|
+
if RUBY_VERSION.split('.').first.to_i >= 3
|
|
22
|
+
def self.efficient_to_s(obj)
|
|
23
|
+
if obj.kind_of?(Symbol)
|
|
24
|
+
obj.name
|
|
25
|
+
else
|
|
26
|
+
obj.to_s
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
def self.efficient_to_s(obj)
|
|
31
|
+
obj.to_s
|
|
32
|
+
end
|
|
33
|
+
end
|
|
19
34
|
end
|
data/playground_models.rb
CHANGED
|
@@ -61,7 +61,7 @@ class MyModel < ActiveRecord::Base
|
|
|
61
61
|
self.table_name = "products"
|
|
62
62
|
|
|
63
63
|
include AttrJson::Record
|
|
64
|
-
include AttrJson::Record::Dirty
|
|
64
|
+
#include AttrJson::Record::Dirty
|
|
65
65
|
|
|
66
66
|
attr_json :str, :string
|
|
67
67
|
attr_json :str_array, :string, array: true
|
|
@@ -76,7 +76,7 @@ end
|
|
|
76
76
|
class Product < StaticProduct
|
|
77
77
|
include AttrJson::Record
|
|
78
78
|
include AttrJson::Record::QueryScopes
|
|
79
|
-
include AttrJson::Record::Dirty
|
|
79
|
+
#include AttrJson::Record::Dirty
|
|
80
80
|
|
|
81
81
|
attr_json :title, :string
|
|
82
82
|
attr_json :rank, :integer
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: attr_json
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jonathan Rochkind
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activerecord
|
|
@@ -16,20 +15,20 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
18
|
+
version: 6.0.0
|
|
20
19
|
- - "<"
|
|
21
20
|
- !ruby/object:Gem::Version
|
|
22
|
-
version: '
|
|
21
|
+
version: '8.2'
|
|
23
22
|
type: :runtime
|
|
24
23
|
prerelease: false
|
|
25
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
26
25
|
requirements:
|
|
27
26
|
- - ">="
|
|
28
27
|
- !ruby/object:Gem::Version
|
|
29
|
-
version:
|
|
28
|
+
version: 6.0.0
|
|
30
29
|
- - "<"
|
|
31
30
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
31
|
+
version: '8.2'
|
|
33
32
|
- !ruby/object:Gem::Dependency
|
|
34
33
|
name: bundler
|
|
35
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -72,20 +71,6 @@ dependencies:
|
|
|
72
71
|
- - "~>"
|
|
73
72
|
- !ruby/object:Gem::Version
|
|
74
73
|
version: '3.7'
|
|
75
|
-
- !ruby/object:Gem::Dependency
|
|
76
|
-
name: database_cleaner
|
|
77
|
-
requirement: !ruby/object:Gem::Requirement
|
|
78
|
-
requirements:
|
|
79
|
-
- - "~>"
|
|
80
|
-
- !ruby/object:Gem::Version
|
|
81
|
-
version: '1.5'
|
|
82
|
-
type: :development
|
|
83
|
-
prerelease: false
|
|
84
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
-
requirements:
|
|
86
|
-
- - "~>"
|
|
87
|
-
- !ruby/object:Gem::Version
|
|
88
|
-
version: '1.5'
|
|
89
74
|
- !ruby/object:Gem::Dependency
|
|
90
75
|
name: yard-activesupport-concern
|
|
91
76
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -144,8 +129,8 @@ dependencies:
|
|
|
144
129
|
version: '1.0'
|
|
145
130
|
description: |-
|
|
146
131
|
ActiveRecord attributes stored serialized in a json column, super smooth.
|
|
147
|
-
|
|
148
|
-
|
|
132
|
+
Typed and cast like Active Record. Supporting nested models, dirty tracking, some querying
|
|
133
|
+
(with postgres jsonb contains), and working smoothy with form builders.
|
|
149
134
|
|
|
150
135
|
Use your database as a typed object store via ActiveRecord, in the same models right next to
|
|
151
136
|
ordinary ActiveRecord column-backed attributes and associations. Your json-serialized attr_json
|
|
@@ -173,15 +158,15 @@ files:
|
|
|
173
158
|
- bin/rspec
|
|
174
159
|
- bin/setup
|
|
175
160
|
- config.ru
|
|
176
|
-
- doc_src/dirty_tracking.md
|
|
177
161
|
- doc_src/forms.md
|
|
178
162
|
- gemfiles/.bundle/config
|
|
179
|
-
- gemfiles/rails_5_0.gemfile
|
|
180
|
-
- gemfiles/rails_5_1.gemfile
|
|
181
|
-
- gemfiles/rails_5_2.gemfile
|
|
182
163
|
- gemfiles/rails_6_0.gemfile
|
|
183
164
|
- gemfiles/rails_6_1.gemfile
|
|
184
165
|
- gemfiles/rails_7_0.gemfile
|
|
166
|
+
- gemfiles/rails_7_1.gemfile
|
|
167
|
+
- gemfiles/rails_7_2.gemfile
|
|
168
|
+
- gemfiles/rails_8_0.gemfile
|
|
169
|
+
- gemfiles/rails_8_1.gemfile
|
|
185
170
|
- gemfiles/rails_edge.gemfile
|
|
186
171
|
- lib/attr_json.rb
|
|
187
172
|
- lib/attr_json/attribute_definition.rb
|
|
@@ -189,12 +174,12 @@ files:
|
|
|
189
174
|
- lib/attr_json/config.rb
|
|
190
175
|
- lib/attr_json/model.rb
|
|
191
176
|
- lib/attr_json/model/cocoon_compat.rb
|
|
177
|
+
- lib/attr_json/model/nested_model_validator.rb
|
|
192
178
|
- lib/attr_json/nested_attributes.rb
|
|
193
179
|
- lib/attr_json/nested_attributes/builder.rb
|
|
194
180
|
- lib/attr_json/nested_attributes/multiparameter_attribute_writer.rb
|
|
195
181
|
- lib/attr_json/nested_attributes/writer.rb
|
|
196
182
|
- lib/attr_json/record.rb
|
|
197
|
-
- lib/attr_json/record/dirty.rb
|
|
198
183
|
- lib/attr_json/record/query_builder.rb
|
|
199
184
|
- lib/attr_json/record/query_scopes.rb
|
|
200
185
|
- lib/attr_json/serialization_coder_from_type.rb
|
|
@@ -210,7 +195,6 @@ licenses:
|
|
|
210
195
|
metadata:
|
|
211
196
|
homepage_uri: https://github.com/jrochkind/attr_json
|
|
212
197
|
source_code_uri: https://github.com/jrochkind/attr_json
|
|
213
|
-
post_install_message:
|
|
214
198
|
rdoc_options: []
|
|
215
199
|
require_paths:
|
|
216
200
|
- lib
|
|
@@ -218,15 +202,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
218
202
|
requirements:
|
|
219
203
|
- - ">="
|
|
220
204
|
- !ruby/object:Gem::Version
|
|
221
|
-
version: 2.
|
|
205
|
+
version: 2.6.0
|
|
222
206
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
223
207
|
requirements:
|
|
224
208
|
- - ">="
|
|
225
209
|
- !ruby/object:Gem::Version
|
|
226
210
|
version: '0'
|
|
227
211
|
requirements: []
|
|
228
|
-
rubygems_version: 3.1
|
|
229
|
-
signing_key:
|
|
212
|
+
rubygems_version: 3.7.1
|
|
230
213
|
specification_version: 4
|
|
231
214
|
summary: ActiveRecord attributes stored serialized in a json column, super smooth.
|
|
232
215
|
test_files: []
|