attr_json 1.5.0 → 2.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +0 -21
- data/Appraisals +0 -27
- data/CHANGELOG.md +40 -1
- data/README.md +60 -52
- 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 +127 -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,10 +151,25 @@ 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
|
@@ -154,6 +193,8 @@ module AttrJson
|
|
154
193
|
# @param type [ActiveModel::Type::Value] An instance of an ActiveModel::Type::Value (or subclass)
|
155
194
|
#
|
156
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`
|
157
198
|
#
|
158
199
|
# @option options [Object] :default (nil) Default value, if a Proc object it will be #call'd
|
159
200
|
# for default.
|
@@ -166,6 +207,8 @@ module AttrJson
|
|
166
207
|
def attr_json(name, type, **options)
|
167
208
|
options.assert_valid_keys(*(AttributeDefinition::VALID_OPTIONS - [:container_attribute] + [:validate]))
|
168
209
|
|
210
|
+
type = _attr_json_maybe_wrap_timezone_aware(type)
|
211
|
+
|
169
212
|
self.attr_json_registry = attr_json_registry.with(
|
170
213
|
AttributeDefinition.new(name.to_sym, type, options.except(:validate))
|
171
214
|
)
|
@@ -200,6 +243,42 @@ module AttrJson
|
|
200
243
|
mod
|
201
244
|
end
|
202
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
|
203
282
|
end
|
204
283
|
|
205
284
|
def initialize(attributes = {})
|
@@ -254,32 +333,64 @@ module AttrJson
|
|
254
333
|
self.class.attribute_names
|
255
334
|
end
|
256
335
|
|
257
|
-
# Override from ActiveModel::Serialization to
|
258
|
-
#
|
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
|
259
341
|
# get properly type-serialized.
|
260
|
-
|
261
|
-
|
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|
|
262
363
|
if attribute_def = self.class.attr_json_registry[key.to_sym]
|
263
364
|
key = attribute_def.store_key
|
264
|
-
if value.kind_of?(Time) || value.kind_of?(DateTime)
|
265
|
-
value = value.utc.change(usec: 0)
|
266
|
-
end
|
267
365
|
|
268
366
|
value = attribute_def.serialize(value)
|
269
367
|
end
|
270
|
-
|
271
|
-
|
272
|
-
|
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
|
273
380
|
end
|
274
381
|
|
275
382
|
# ActiveRecord JSON serialization will insist on calling
|
276
383
|
# this, instead of the specified type's #serialize, at least in some cases.
|
277
384
|
# So it's important we define it -- the default #as_json added by ActiveSupport
|
278
385
|
# will serialize all instance variables, which is not what we want.
|
279
|
-
|
280
|
-
|
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)
|
281
391
|
end
|
282
392
|
|
393
|
+
|
283
394
|
# We deep_dup on #to_h, you want attributes unduped, ask for #attributes.
|
284
395
|
def to_h
|
285
396
|
attributes.deep_dup
|
@@ -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
|