attr_json 1.4.1 → 2.0.0.rc1
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 +4 -21
- data/.github/workflows/future_rails_ci.yml +2 -2
- data/Appraisals +0 -27
- data/CHANGELOG.md +50 -1
- data/Gemfile +2 -2
- data/README.md +62 -54
- data/attr_json.gemspec +4 -4
- data/doc_src/forms.md +3 -14
- data/lib/attr_json/attribute_definition.rb +49 -13
- data/lib/attr_json/config.rb +1 -2
- data/lib/attr_json/model.rb +158 -16
- data/lib/attr_json/nested_attributes/writer.rb +4 -6
- data/lib/attr_json/nested_attributes.rb +5 -1
- data/lib/attr_json/record.rb +95 -74
- data/lib/attr_json/type/array.rb +10 -3
- data/lib/attr_json/type/model.rb +13 -4
- data/lib/attr_json/version.rb +1 -1
- data/lib/attr_json.rb +0 -5
- data/playground_models.rb +2 -2
- metadata +9 -14
- data/doc_src/dirty_tracking.md +0 -155
- data/gemfiles/rails_5_0.gemfile +0 -20
- data/gemfiles/rails_5_1.gemfile +0 -19
- data/gemfiles/rails_5_2.gemfile +0 -19
- data/lib/attr_json/record/dirty.rb +0 -287
data/lib/attr_json/model.rb
CHANGED
@@ -20,7 +20,7 @@ module AttrJson
|
|
20
20
|
# Meant for use as an attribute of a AttrJson::Record. Can be nested,
|
21
21
|
# AttrJson::Models can have attributes that are other AttrJson::Models.
|
22
22
|
#
|
23
|
-
# @note Includes ActiveModel::Model whether you like it or not.
|
23
|
+
# @note Includes ActiveModel::Model whether you like it or not.
|
24
24
|
#
|
25
25
|
# You can control what happens if you set an unknown key (one that you didn't
|
26
26
|
# register with `attr_json`) with the config attribute `attr_json_config(unknown_key:)`.
|
@@ -47,6 +47,21 @@ module AttrJson
|
|
47
47
|
# #...
|
48
48
|
# end
|
49
49
|
#
|
50
|
+
# ## Date-type timezone conversion
|
51
|
+
#
|
52
|
+
# By default, AttrJson::Model date/time attributes will be
|
53
|
+
# [ActiveRecord timezone-aware](https://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html)
|
54
|
+
# based on settings of `config.active_record.time_zone_aware_attributes` and
|
55
|
+
# `ActiveRecord::Base.time_zone_aware_types`.
|
56
|
+
#
|
57
|
+
# If you'd like to override this, you can set:
|
58
|
+
#
|
59
|
+
# ```
|
60
|
+
# attr_json_config(time_zone_aware_attributes: true)
|
61
|
+
# attr_json_config(time_zone_aware_attributes: false)
|
62
|
+
# attr_json_config(time_zone_aware_attributes: [:datetime, :time]) # custom list of types
|
63
|
+
# ```
|
64
|
+
#
|
50
65
|
# ## ActiveRecord `serialize`
|
51
66
|
#
|
52
67
|
# If you want to map a single AttrJson::Model to a json/jsonb column, you
|
@@ -65,6 +80,15 @@ module AttrJson
|
|
65
80
|
# serialize :some_json_column, ValueModel.to_serialize_coder
|
66
81
|
# end
|
67
82
|
#
|
83
|
+
# # Strip nils
|
84
|
+
#
|
85
|
+
# When embedded in an `attr_json` attribute, models are normally serialized with `nil` values
|
86
|
+
# stripped from hash where possible, for a more compact representation.
|
87
|
+
# This can be set differently in the type.
|
88
|
+
#
|
89
|
+
# attr_json :lang_and_value, LangAndValue.to_type(strip_nils: false)
|
90
|
+
#
|
91
|
+
# See #serializable_hash docs for possible values.
|
68
92
|
module Model
|
69
93
|
extend ActiveSupport::Concern
|
70
94
|
|
@@ -106,7 +130,7 @@ module AttrJson
|
|
106
130
|
# The inverse of model#serializable_hash -- re-hydrates a serialized hash to a model.
|
107
131
|
#
|
108
132
|
# 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.
|
133
|
+
# like store_keys, and properly calling deserialize (rather than cast) on the underlying types.
|
110
134
|
#
|
111
135
|
# @example Model.new_from_serializable(hash)
|
112
136
|
def new_from_serializable(attributes = {})
|
@@ -127,14 +151,40 @@ module AttrJson
|
|
127
151
|
self.new(attributes)
|
128
152
|
end
|
129
153
|
|
130
|
-
|
131
|
-
|
154
|
+
# @returns ActiveModel::Type suitable for including this model in
|
155
|
+
# an AttrJson::Record or ::Model attribute
|
156
|
+
#
|
157
|
+
# @param strip_nils [Boolean,Symbol] [true,false,:safely] as a type,
|
158
|
+
# should we strip nils when serializing? By default this type strips
|
159
|
+
# nils in :safely mode. See AttrJson::Model#serializable_hash
|
160
|
+
def to_type(strip_nils: :safely)
|
161
|
+
@type ||= AttrJson::Type::Model.new(self, strip_nils: strip_nils)
|
132
162
|
end
|
133
163
|
|
164
|
+
# An ActiveModel::Type that can be used to serialize this model
|
165
|
+
# across an entire JSON(b) column.
|
166
|
+
#
|
167
|
+
# @example using standard ActiveRecord `serialize` feature.
|
168
|
+
#
|
169
|
+
# class MyTable < ApplicationRecord
|
170
|
+
# serialize :some_json_column, MyModel.to_serialization_coder
|
171
|
+
# end
|
172
|
+
#
|
134
173
|
def to_serialization_coder
|
135
174
|
@serialization_coder ||= AttrJson::SerializationCoderFromType.new(to_type)
|
136
175
|
end
|
137
176
|
|
177
|
+
# like the ActiveModel::Attributes method
|
178
|
+
def attribute_names
|
179
|
+
attr_json_registry.attribute_names
|
180
|
+
end
|
181
|
+
|
182
|
+
# like the ActiveModel::Attributes method, hash with name keys, and ActiveModel::Type values
|
183
|
+
def attribute_types
|
184
|
+
attribute_names.collect { |name| [name.to_s, attr_json_registry.type_for_attribute(name)]}.to_h
|
185
|
+
end
|
186
|
+
|
187
|
+
|
138
188
|
# Type can be an instance of an ActiveModel::Type::Value subclass, or a symbol that will
|
139
189
|
# be looked up in `ActiveModel::Type.lookup`
|
140
190
|
#
|
@@ -143,6 +193,8 @@ module AttrJson
|
|
143
193
|
# @param type [ActiveModel::Type::Value] An instance of an ActiveModel::Type::Value (or subclass)
|
144
194
|
#
|
145
195
|
# @option options [Boolean] :array (false) Make this attribute an array of given type.
|
196
|
+
# Array types default to an empty array. If you want to turn that off, you can add
|
197
|
+
# `default: AttrJson::AttributeDefinition::NO_DEFAULT_PROVIDED`
|
146
198
|
#
|
147
199
|
# @option options [Object] :default (nil) Default value, if a Proc object it will be #call'd
|
148
200
|
# for default.
|
@@ -155,6 +207,8 @@ module AttrJson
|
|
155
207
|
def attr_json(name, type, **options)
|
156
208
|
options.assert_valid_keys(*(AttributeDefinition::VALID_OPTIONS - [:container_attribute] + [:validate]))
|
157
209
|
|
210
|
+
type = _attr_json_maybe_wrap_timezone_aware(type)
|
211
|
+
|
158
212
|
self.attr_json_registry = attr_json_registry.with(
|
159
213
|
AttributeDefinition.new(name.to_sym, type, options.except(:validate))
|
160
214
|
)
|
@@ -189,6 +243,42 @@ module AttrJson
|
|
189
243
|
mod
|
190
244
|
end
|
191
245
|
end
|
246
|
+
|
247
|
+
# wrap in ActiveRecord type for timezone-awareness/conversion, if needed
|
248
|
+
# We at present only need this in AttrJson::Model cause in AttrJson::Record,
|
249
|
+
# our sync with ActiveRecord attributes takes care of it for us.
|
250
|
+
# See https://github.com/rails/rails/blob/v7.0.4/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb#L78
|
251
|
+
#
|
252
|
+
# For now we use ActiveRecord::Base state to decide.
|
253
|
+
#
|
254
|
+
# We have to turn a symbol type into a real object type if we want to wrap it -- we will
|
255
|
+
# return an actual ActiveModel::Value::Type either way, converting from symbol!
|
256
|
+
#
|
257
|
+
# That *wrapped* type will be the new type, registered with AttributeDefinition
|
258
|
+
# and then with Rails `attribute`, to provide timezone conversion.
|
259
|
+
def _attr_json_maybe_wrap_timezone_aware(type)
|
260
|
+
type = AttributeDefinition.lookup_type(type)
|
261
|
+
|
262
|
+
if self.attr_json_config.time_zone_aware_attributes.nil?
|
263
|
+
# nil config means use ActiveRecord::Base
|
264
|
+
aware = ActiveRecord::Base.time_zone_aware_attributes
|
265
|
+
aware_types = ActiveRecord::Base.time_zone_aware_types
|
266
|
+
elsif self.attr_json_config.time_zone_aware_attributes.kind_of?(Array)
|
267
|
+
# Array config means we're doing it, and these are the types
|
268
|
+
aware = true
|
269
|
+
aware_types = self.attr_json_config.time_zone_aware_attributes
|
270
|
+
else
|
271
|
+
# boolean config, types from ActiveRecord::Base
|
272
|
+
aware = !!self.attr_json_config.time_zone_aware_attributes
|
273
|
+
aware_types = ActiveRecord::Base.time_zone_aware_types
|
274
|
+
end
|
275
|
+
|
276
|
+
if aware && aware_types.include?(type.type)
|
277
|
+
type = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(type)
|
278
|
+
end
|
279
|
+
|
280
|
+
type
|
281
|
+
end
|
192
282
|
end
|
193
283
|
|
194
284
|
def initialize(attributes = {})
|
@@ -197,6 +287,12 @@ module AttrJson
|
|
197
287
|
fill_in_defaults!
|
198
288
|
end
|
199
289
|
|
290
|
+
# inspired by https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/activemodel/lib/active_model/attributes.rb
|
291
|
+
def initialize_dup(other) # :nodoc:
|
292
|
+
@attributes = @attributes.deep_dup
|
293
|
+
super
|
294
|
+
end
|
295
|
+
|
200
296
|
def attributes
|
201
297
|
@attributes ||= {}
|
202
298
|
end
|
@@ -232,32 +328,69 @@ module AttrJson
|
|
232
328
|
self.class.attr_json_registry.has_attribute?(str)
|
233
329
|
end
|
234
330
|
|
235
|
-
#
|
236
|
-
|
331
|
+
# like the ActiveModel::Attributes method
|
332
|
+
def attribute_names
|
333
|
+
self.class.attribute_names
|
334
|
+
end
|
335
|
+
|
336
|
+
# Override from ActiveModel::Serialization to:
|
337
|
+
#
|
338
|
+
# * handle store_key settings
|
339
|
+
#
|
340
|
+
# * #serialize by type to make sure any values set directly on hash still
|
237
341
|
# get properly type-serialized.
|
238
|
-
|
239
|
-
|
342
|
+
#
|
343
|
+
# * custom logic for keeping nil values out of serialization to be more compact
|
344
|
+
#
|
345
|
+
# @param strip_nils [:symbol, Boolean] (default false) Should we keep keys with
|
346
|
+
# `nil` values out of the serialization entirely? You might want to to keep
|
347
|
+
# your in-database serialization compact. By default this method does not -- but
|
348
|
+
# by default AttrJson::Type::Model sends `:safely` when serializing.
|
349
|
+
# * false => do not strip nils
|
350
|
+
# * :safely => strip nils only when there is no default value for the attribute,
|
351
|
+
# so `nil` can still override the default value
|
352
|
+
# * true => strip nils even if there is a default value -- in AttrJson
|
353
|
+
# context, this means the default will be reapplied over nil on
|
354
|
+
# every de-serialization!
|
355
|
+
def serializable_hash(options=nil)
|
356
|
+
strip_nils = options&.has_key?(:strip_nils) ? options.delete(:strip_nils) : false
|
357
|
+
|
358
|
+
unless [true, false, :safely].include?(strip_nils)
|
359
|
+
raise ArgumentError, ":strip_nils must be true, false, or :safely"
|
360
|
+
end
|
361
|
+
|
362
|
+
super(options).collect do |key, value|
|
240
363
|
if attribute_def = self.class.attr_json_registry[key.to_sym]
|
241
364
|
key = attribute_def.store_key
|
242
|
-
if value.kind_of?(Time) || value.kind_of?(DateTime)
|
243
|
-
value = value.utc.change(usec: 0)
|
244
|
-
end
|
245
365
|
|
246
366
|
value = attribute_def.serialize(value)
|
247
367
|
end
|
248
|
-
|
249
|
-
|
250
|
-
|
368
|
+
|
369
|
+
# strip_nils handling
|
370
|
+
if value.nil? && (
|
371
|
+
(strip_nils == :safely && !attribute_def&.has_default?) ||
|
372
|
+
strip_nils == true )
|
373
|
+
then
|
374
|
+
# do not include in serializable_hash
|
375
|
+
nil
|
376
|
+
else
|
377
|
+
[key, value]
|
378
|
+
end
|
379
|
+
end.compact.to_h
|
251
380
|
end
|
252
381
|
|
253
382
|
# ActiveRecord JSON serialization will insist on calling
|
254
383
|
# this, instead of the specified type's #serialize, at least in some cases.
|
255
384
|
# So it's important we define it -- the default #as_json added by ActiveSupport
|
256
385
|
# will serialize all instance variables, which is not what we want.
|
257
|
-
|
258
|
-
|
386
|
+
#
|
387
|
+
# @param strip_nils [:symbol, Boolean] (default false) [true, false, :safely],
|
388
|
+
# see #serializable_hash
|
389
|
+
def as_json(options=nil)
|
390
|
+
serializable_hash(options)
|
259
391
|
end
|
260
392
|
|
393
|
+
|
261
394
|
# We deep_dup on #to_h, you want attributes unduped, ask for #attributes.
|
262
395
|
def to_h
|
263
396
|
attributes.deep_dup
|
@@ -278,6 +411,15 @@ module AttrJson
|
|
278
411
|
false
|
279
412
|
end
|
280
413
|
|
414
|
+
# like ActiveModel::Attributes at
|
415
|
+
# https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/activemodel/lib/active_model/attributes.rb#L120
|
416
|
+
#
|
417
|
+
# is not a full deep freeze
|
418
|
+
def freeze
|
419
|
+
attributes.freeze unless frozen?
|
420
|
+
super
|
421
|
+
end
|
422
|
+
|
281
423
|
private
|
282
424
|
|
283
425
|
def fill_in_defaults!
|
@@ -15,12 +15,10 @@ module AttrJson
|
|
15
15
|
delegate :nested_attributes_options, to: :model
|
16
16
|
|
17
17
|
def assign_nested_attributes(attributes)
|
18
|
-
if attr_def.
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
assign_nested_attributes_for_model_array(attributes)
|
23
|
-
end
|
18
|
+
if attr_def.array_of_primitive_type?
|
19
|
+
assign_nested_attributes_for_primitive_array(attributes)
|
20
|
+
elsif attr_def.array_type?
|
21
|
+
assign_nested_attributes_for_model_array(attributes)
|
24
22
|
else
|
25
23
|
assign_nested_attributes_for_single_model(attributes)
|
26
24
|
end
|
@@ -65,6 +65,10 @@ module AttrJson
|
|
65
65
|
raise ArgumentError, "No attr_json found for name '#{attr_name}'. Has it been defined yet?"
|
66
66
|
end
|
67
67
|
|
68
|
+
unless attr_def.array_type? || attr_def.single_model_type?
|
69
|
+
raise TypeError, "attr_json_accepts_nested_attributes_for is only for array or nested model types; `#{attr_name}` is type #{attr_def.type.type.inspect}"
|
70
|
+
end
|
71
|
+
|
68
72
|
# We're sharing AR class attr in an AR, or using our own in a Model.
|
69
73
|
nested_attributes_options = self.nested_attributes_options.dup
|
70
74
|
nested_attributes_options[attr_name.to_sym] = options
|
@@ -80,7 +84,7 @@ module AttrJson
|
|
80
84
|
end
|
81
85
|
|
82
86
|
# No build method for our wacky array of primitive type.
|
83
|
-
if options[:define_build_method] && !
|
87
|
+
if options[:define_build_method] && !attr_def.array_of_primitive_type?
|
84
88
|
_attr_jsons_module.module_eval do
|
85
89
|
build_method_name = "build_#{attr_name.to_s.singularize}"
|
86
90
|
if method_defined?(build_method_name)
|
data/lib/attr_json/record.rb
CHANGED
@@ -24,35 +24,51 @@ module AttrJson
|
|
24
24
|
|
25
25
|
class_attribute :attr_json_registry, instance_accessor: false
|
26
26
|
self.attr_json_registry = AttrJson::AttributeDefinition::Registry.new
|
27
|
-
end
|
28
27
|
|
29
|
-
|
28
|
+
# Ensure that rails attributes tracker knows about values we just fetched
|
29
|
+
after_initialize do
|
30
|
+
attr_json_sync_to_rails_attributes
|
31
|
+
end
|
32
|
+
|
33
|
+
# After a safe, rails attribute dirty tracking ends up re-creating
|
34
|
+
# new objects for attribute values, so we need to sync again
|
35
|
+
# so mutation effects both.
|
36
|
+
after_save do
|
37
|
+
attr_json_sync_to_rails_attributes
|
38
|
+
end
|
39
|
+
end
|
30
40
|
|
31
|
-
#
|
32
|
-
# https://github.com/rails/rails/blob/v5.2.3/activerecord/lib/active_record/attribute_methods/query.rb#L12
|
41
|
+
# Sync all values FROM the json_attributes json column TO rails attributes
|
33
42
|
#
|
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.
|
43
|
+
# If values have for some reason gotten out of sync this will make them the
|
44
|
+
# identical objects again, with the container hash value being the source.
|
39
45
|
#
|
40
|
-
#
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
46
|
+
# In some cases, the values may already be equivalent but different objects --
|
47
|
+
# This is meant to ensure they are the _same object_ in both places, so
|
48
|
+
# mutation of mutable object will effect both places, for instance for dirty
|
49
|
+
# tracking.
|
50
|
+
def attr_json_sync_to_rails_attributes
|
51
|
+
self.class.attr_json_registry.attribute_names.each do |attr_name|
|
52
|
+
begin
|
53
|
+
attribute_def = self.class.attr_json_registry.fetch(attr_name.to_sym)
|
54
|
+
json_value = public_send(attribute_def.container_attribute)
|
55
|
+
value = json_value[attribute_def.store_key]
|
56
|
+
|
57
|
+
if value
|
58
|
+
# TODO, can we just make this use the setter?
|
59
|
+
write_attribute(attr_name, value)
|
60
|
+
|
61
|
+
clear_attribute_change(attr_name) if persisted?
|
62
|
+
|
63
|
+
# writing and clearning will result in a new object stored in
|
64
|
+
# rails attributes, we want
|
65
|
+
# to make sure the exact same object is in the json attribute,
|
66
|
+
# so in-place mutation changes to it are reflected in both places.
|
67
|
+
json_value[attribute_def.store_key] = read_attribute(attr_name)
|
68
|
+
end
|
69
|
+
rescue AttrJson::Type::Model::BadCast, AttrJson::Type::PolymorphicModel::TypeError => e
|
70
|
+
# There was bad data in the DB, we're just going to skip the Rails attribute sync.
|
71
|
+
# Should we log?
|
56
72
|
end
|
57
73
|
end
|
58
74
|
end
|
@@ -93,7 +109,8 @@ module AttrJson
|
|
93
109
|
end
|
94
110
|
|
95
111
|
|
96
|
-
|
112
|
+
# Registers an attr_json attribute, and a Rails attribute covering it.
|
113
|
+
#
|
97
114
|
# Type can be a symbol that will be looked up in `ActiveModel::Type.lookup`,
|
98
115
|
# or an ActiveModel:::Type::Value).
|
99
116
|
#
|
@@ -102,6 +119,8 @@ module AttrJson
|
|
102
119
|
# @param type [ActiveModel::Type::Value] An instance of an ActiveModel::Type::Value (or subclass)
|
103
120
|
#
|
104
121
|
# @option options [Boolean] :array (false) Make this attribute an array of given type.
|
122
|
+
# Array types default to an empty array. If you want to turn that off, you can add
|
123
|
+
# `default: AttrJson::AttributeDefinition::NO_DEFAULT_PROVIDED`
|
105
124
|
#
|
106
125
|
# @option options [Object] :default (nil) Default value, if a Proc object it will be #call'd
|
107
126
|
# for default.
|
@@ -113,22 +132,22 @@ module AttrJson
|
|
113
132
|
# json(b) ActiveRecord attribute/column to serialize as a key in. Defaults to
|
114
133
|
# `attr_json_config.default_container_attribute`, which defaults to `:json_attributes`
|
115
134
|
#
|
116
|
-
# @option options [Boolean] :validate (true)
|
117
|
-
#
|
135
|
+
# @option options [Boolean] :validate (true) validation errors on nested models in the attributes
|
136
|
+
# should post up to self similar to Rails ActiveRecord::Validations::AssociatedValidator on
|
137
|
+
# associated objects.
|
138
|
+
#
|
139
|
+
# @option options [Boolean,Hash] :accepts_nested_attributes. If true, equivalent
|
140
|
+
# of writing `attr_json_accepts_nested_attributes :attribute_name`. If value is a hash,
|
141
|
+
# then same, but with hash as options to `attr_json_accepts_nested_attributes`.
|
142
|
+
# Default taken from `attr_json_config.default_accepts_nested_attributes`, for
|
143
|
+
# array or model types where it is applicable.
|
118
144
|
#
|
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
145
|
def attr_json(name, type, **options)
|
125
146
|
options = {
|
126
|
-
rails_attribute: self.attr_json_config.default_rails_attribute,
|
127
147
|
validate: true,
|
128
148
|
container_attribute: self.attr_json_config.default_container_attribute,
|
129
|
-
accepts_nested_attributes: self.attr_json_config.default_accepts_nested_attributes
|
130
149
|
}.merge!(options)
|
131
|
-
options.assert_valid_keys(AttributeDefinition::VALID_OPTIONS + [:validate, :
|
150
|
+
options.assert_valid_keys(AttributeDefinition::VALID_OPTIONS + [:validate, :accepts_nested_attributes])
|
132
151
|
container_attribute = options[:container_attribute]
|
133
152
|
|
134
153
|
# TODO arg check container_attribute make sure it exists. Hard cause
|
@@ -150,63 +169,65 @@ module AttrJson
|
|
150
169
|
end
|
151
170
|
|
152
171
|
self.attr_json_registry = attr_json_registry.with(
|
153
|
-
AttributeDefinition.new(name.to_sym, type, options.except(:
|
172
|
+
AttributeDefinition.new(name.to_sym, type, options.except(:validate, :accepts_nested_attributes))
|
154
173
|
)
|
155
174
|
|
156
|
-
# By default, automatically validate nested models
|
175
|
+
# By default, automatically validate nested models, allowing nils.
|
157
176
|
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])
|
177
|
+
# implementation adopted from:
|
178
|
+
# https://github.com/rails/rails/blob/v7.0.4.1/activerecord/lib/active_record/validations/associated.rb#L6-L10
|
179
|
+
#
|
180
|
+
# but had to customize to allow nils in an array through
|
181
|
+
validates_each name.to_sym do |record, attr, value|
|
182
|
+
if Array(value).reject { |element| element.nil? || element.valid? }.any?
|
183
|
+
record.errors.add(attr, :invalid, value: value)
|
177
184
|
end
|
178
185
|
end
|
179
186
|
end
|
180
187
|
|
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.
|
188
|
+
# Register as a Rails attribute
|
189
|
+
attr_json_definition = attr_json_registry[name]
|
190
|
+
attribute_args = attr_json_definition.has_default? ? { default: attr_json_definition.default_argument } : {}
|
191
|
+
self.attribute name.to_sym, attr_json_definition.type, **attribute_args
|
189
192
|
|
193
|
+
# For getter and setter, we consider the container has the "canonical" data location.
|
194
|
+
# But setter also writes to rails attribute, and tries to keep them in sync with the
|
195
|
+
# *same object*, so mutations happen to both places.
|
196
|
+
#
|
197
|
+
# This began roughly modelled on approach of Rail sstore_accessor implementation:
|
198
|
+
# https://github.com/rails/rails/blob/74c3e43fba458b9b863d27f0c45fd2d8dc603cbc/activerecord/lib/active_record/store.rb#L90-L96
|
199
|
+
#
|
200
|
+
# ...But wound up with lots of evolution to try to get dirty tracking working as well
|
201
|
+
# as we could -- without a completely custom separate dirty tracking implementation
|
202
|
+
# like store_accessor tries!
|
203
|
+
_attr_jsons_module.module_eval do
|
190
204
|
define_method("#{name}=") do |value|
|
191
|
-
super(value)
|
205
|
+
super(value) # should write to rails attribute
|
206
|
+
|
207
|
+
# write to container hash, with value read from attribute to try to keep objects
|
208
|
+
# sync'd to exact same object in rails attribute and container hash.
|
192
209
|
attribute_def = self.class.attr_json_registry.fetch(name.to_sym)
|
193
|
-
public_send(attribute_def.container_attribute)[attribute_def.store_key] =
|
210
|
+
public_send(attribute_def.container_attribute)[attribute_def.store_key] = read_attribute(name)
|
194
211
|
end
|
195
212
|
|
196
213
|
define_method("#{name}") do
|
214
|
+
# read from container hash -- we consider that the canonical location.
|
197
215
|
attribute_def = self.class.attr_json_registry.fetch(name.to_sym)
|
198
216
|
public_send(attribute_def.container_attribute)[attribute_def.store_key]
|
199
217
|
end
|
218
|
+
end
|
200
219
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
220
|
+
accepts_nested_attributes = if options.has_key?(:accepts_nested_attributes)
|
221
|
+
options[:accepts_nested_attributes]
|
222
|
+
elsif attr_json_definition.single_model_type? || attr_json_definition.array_type?
|
223
|
+
# use configured default only if we have a type appropriate for it!
|
224
|
+
self.attr_json_config.default_accepts_nested_attributes
|
225
|
+
else
|
226
|
+
false
|
205
227
|
end
|
206
228
|
|
207
|
-
|
208
|
-
|
209
|
-
options = options[:accepts_nested_attributes] == true ? {} : options[:accepts_nested_attributes]
|
229
|
+
if accepts_nested_attributes
|
230
|
+
options = accepts_nested_attributes == true ? {} : accepts_nested_attributes
|
210
231
|
self.attr_json_accepts_nested_attributes_for name, **options
|
211
232
|
end
|
212
233
|
end
|
data/lib/attr_json/type/array.rb
CHANGED
@@ -30,6 +30,10 @@ module AttrJson
|
|
30
30
|
convert_to_array(value).collect { |v| base_type.deserialize(v) }
|
31
31
|
end
|
32
32
|
|
33
|
+
def changed_in_place?(raw_old_value, new_value)
|
34
|
+
serialize(new_value) != raw_old_value
|
35
|
+
end
|
36
|
+
|
33
37
|
# This is used only by our own keypath-chaining query stuff.
|
34
38
|
def value_for_contains_query(key_path_arr, value)
|
35
39
|
[
|
@@ -41,10 +45,13 @@ module AttrJson
|
|
41
45
|
]
|
42
46
|
end
|
43
47
|
|
44
|
-
#
|
45
|
-
#
|
48
|
+
# Soft-deprecated. You probably want to use
|
49
|
+
#
|
50
|
+
# AttrJson::AttributeDefinition#array_of_primitive_type?
|
51
|
+
#
|
52
|
+
# instead where possible.
|
46
53
|
def base_type_primitive?
|
47
|
-
!
|
54
|
+
! AttrJson::AttributeDefinition.single_model_type?(base_type)
|
48
55
|
end
|
49
56
|
|
50
57
|
protected
|
data/lib/attr_json/type/model.rb
CHANGED
@@ -8,13 +8,22 @@ module AttrJson
|
|
8
8
|
# but normally that's only done in AttrJson::Model.to_type, there isn't
|
9
9
|
# an anticipated need to create from any other place.
|
10
10
|
#
|
11
|
+
#
|
11
12
|
class Model < ::ActiveModel::Type::Value
|
12
13
|
class BadCast < ArgumentError ; end
|
13
14
|
|
14
|
-
attr_accessor :model
|
15
|
-
|
15
|
+
attr_accessor :model, :strip_nils
|
16
|
+
|
17
|
+
# @param model [AttrJson::Model] the model _class_ object
|
18
|
+
# @param strip_nils [Symbol,Boolean] [true, false, or :safely]
|
19
|
+
# (default :safely), As a type, should we strip nils when serialiing?
|
20
|
+
# This value passed to AttrJson::Model#serialized_hash(strip_nils).
|
21
|
+
# by default it's :safely, we strip nils when it can be done safely
|
22
|
+
# to preserve default overrides.
|
23
|
+
def initialize(model, strip_nils: :safely)
|
16
24
|
#TODO type check, it really better be a AttrJson::Model. maybe?
|
17
25
|
@model = model
|
26
|
+
@strip_nils = strip_nils
|
18
27
|
end
|
19
28
|
|
20
29
|
def type
|
@@ -51,9 +60,9 @@ module AttrJson
|
|
51
60
|
if v.nil?
|
52
61
|
nil
|
53
62
|
elsif v.kind_of?(model)
|
54
|
-
v.serializable_hash
|
63
|
+
v.serializable_hash(strip_nils: strip_nils)
|
55
64
|
else
|
56
|
-
(cast_v = cast(v)) && cast_v.serializable_hash
|
65
|
+
(cast_v = cast(v)) && cast_v.serializable_hash(strip_nils: strip_nils)
|
57
66
|
end
|
58
67
|
end
|
59
68
|
|
data/lib/attr_json/version.rb
CHANGED
data/lib/attr_json.rb
CHANGED
@@ -9,11 +9,6 @@ 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
|
|
19
14
|
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
|