attr_json 1.5.0 → 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 +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
|