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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'attr_json/type/array'
|
|
2
4
|
|
|
3
5
|
module AttrJson
|
|
@@ -34,23 +36,14 @@
|
|
|
34
36
|
|
|
35
37
|
@default = if options.has_key?(:default)
|
|
36
38
|
options[:default]
|
|
39
|
+
elsif options[:array] == true
|
|
40
|
+
-> { [] }
|
|
37
41
|
else
|
|
38
42
|
NO_DEFAULT_PROVIDED
|
|
39
43
|
end
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# seems to have a bug with multi-param assignment. Mostly they return
|
|
44
|
-
# the same types, but ActiveRecord::Type::Date works with multi-param assignment.
|
|
45
|
-
#
|
|
46
|
-
# We pass `adapter: nil` to avoid triggering a db connection.
|
|
47
|
-
# See: https://github.com/jrochkind/attr_json/issues/41
|
|
48
|
-
# This is at the "cost" of not using any adapter-specific types... which
|
|
49
|
-
# maybe preferable anyway?
|
|
50
|
-
type = ActiveRecord::Type.lookup(type, adapter: nil)
|
|
51
|
-
elsif ! type.is_a? ActiveModel::Type::Value
|
|
52
|
-
raise ArgumentError, "Second argument (#{type}) must be a symbol or instance of an ActiveModel::Type::Value subclass"
|
|
53
|
-
end
|
|
45
|
+
type = self.class.lookup_type(type)
|
|
46
|
+
|
|
54
47
|
@type = (options[:array] == true ? AttrJson::Type::Array.new(type) : type)
|
|
55
48
|
end
|
|
56
49
|
|
|
@@ -71,7 +64,7 @@
|
|
|
71
64
|
end
|
|
72
65
|
|
|
73
66
|
def store_key
|
|
74
|
-
(@store_key || name)
|
|
67
|
+
AttrJson.efficient_to_s(@store_key || name)
|
|
75
68
|
end
|
|
76
69
|
|
|
77
70
|
def has_default?
|
|
@@ -102,5 +95,49 @@
|
|
|
102
95
|
def array_type?
|
|
103
96
|
type.is_a? AttrJson::Type::Array
|
|
104
97
|
end
|
|
98
|
+
|
|
99
|
+
# Used for figuring out appropriate behavior for nested attribute implementation among
|
|
100
|
+
# other places. true if type is a nested model, either straight or polymorphic
|
|
101
|
+
def single_model_type?
|
|
102
|
+
self.class.single_model_type?(type)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.single_model_type?(arg_type)
|
|
106
|
+
arg_type.is_a?(AttrJson::Type::Model) || arg_type.is_a?(AttrJson::Type::PolymorphicModel)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Used for figuring out appropriate behavior in nested attribute implementation among
|
|
110
|
+
# other places. true if type is an array of things that are not nested models.
|
|
111
|
+
def array_of_primitive_type?
|
|
112
|
+
array_type? && !self.class.single_model_type?(type.base_type)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Can look up a symbol to a type, or leave a type alone, or raise if it's neither.
|
|
116
|
+
# Extracted into a method, so it can be called from AttrJson::Model#attr_json, for
|
|
117
|
+
# some timezone aware shenanigans.
|
|
118
|
+
def self.lookup_type(type)
|
|
119
|
+
if type.is_a? Symbol
|
|
120
|
+
# ActiveModel::Type.lookup may make more sense, but ActiveModel::Type::Date
|
|
121
|
+
# seems to have a bug with multi-param assignment. Mostly they return
|
|
122
|
+
# the same types, but ActiveRecord::Type::Date works with multi-param assignment.
|
|
123
|
+
#
|
|
124
|
+
# We pass `adapter: nil` to avoid triggering a db connection.
|
|
125
|
+
# See: https://github.com/jrochkind/attr_json/issues/41
|
|
126
|
+
# This is at the "cost" of not using any adapter-specific types... which
|
|
127
|
+
# maybe preferable anyway?
|
|
128
|
+
#
|
|
129
|
+
# AND we add precision for datetime/time types... since we're using Rails json
|
|
130
|
+
# serializing, we're kind of stuck with this precision in current implementation.
|
|
131
|
+
lookup_kwargs = { adapter: nil }
|
|
132
|
+
if type == :datetime || type == :time
|
|
133
|
+
lookup_kwargs[:precision] = ActiveSupport::JSON::Encoding.time_precision
|
|
134
|
+
end
|
|
135
|
+
type = ActiveRecord::Type.lookup(type, **lookup_kwargs)
|
|
136
|
+
elsif !(type.is_a?(ActiveModel::Type::Value) || type.is_a?(ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter))
|
|
137
|
+
raise ArgumentError, "Second argument (#{type}) must be a symbol or instance of an ActiveModel::Type::Value subclass"
|
|
138
|
+
end
|
|
139
|
+
type
|
|
140
|
+
end
|
|
141
|
+
|
|
105
142
|
end
|
|
106
143
|
end
|
data/lib/attr_json/config.rb
CHANGED
|
@@ -5,18 +5,17 @@ module AttrJson
|
|
|
5
5
|
class Config
|
|
6
6
|
RECORD_ALLOWED_KEYS = %i{
|
|
7
7
|
default_container_attribute
|
|
8
|
-
default_rails_attribute
|
|
9
8
|
default_accepts_nested_attributes
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
MODEL_ALLOWED_KEYS = %i{
|
|
13
12
|
unknown_key
|
|
14
13
|
bad_cast
|
|
14
|
+
time_zone_aware_attributes
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
DEFAULTS = {
|
|
18
18
|
default_container_attribute: "json_attributes",
|
|
19
|
-
default_rails_attribute: false,
|
|
20
19
|
unknown_key: :raise
|
|
21
20
|
}
|
|
22
21
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module AttrJson
|
|
2
|
+
module Model
|
|
3
|
+
# Used to validate an attribute in an AttrJson::Model whose values are other models, when
|
|
4
|
+
# you want validation errors on the nested models to post up.
|
|
5
|
+
#
|
|
6
|
+
# This is based on ActiveRecord's own ActiveRecord::Validations::AssociatedValidator, and actually forked
|
|
7
|
+
# from it at https://github.com/rails/rails/blob/e37adfed4eff3b43350ec87222a922e9c72d9c1b/activerecord/lib/active_record/validations/associated.rb
|
|
8
|
+
#
|
|
9
|
+
# We used to simply use an ActiveRecord::Validations::AssociatedValidator, but as of https://github.com/jrochkind/attr_json/pull/220 (e1e798142d)
|
|
10
|
+
# it got ActiveRecord-specific functionality that no longer worked with our use case.
|
|
11
|
+
#
|
|
12
|
+
# No problem, the implementation is simple, we can provide it here, based on the last version that did work.
|
|
13
|
+
class NestedModelValidator < ActiveModel::EachValidator
|
|
14
|
+
def validate_each(record, attribute, value)
|
|
15
|
+
if Array(value).reject { |r| valid_object?(r) }.any?
|
|
16
|
+
record.errors.add(attribute, :invalid, **options.merge(value: value))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
def valid_object?(record)
|
|
22
|
+
#(record.respond_to?(:marked_for_destruction?) && record.marked_for_destruction?) || record.valid?
|
|
23
|
+
record.valid?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/attr_json/model.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'active_support/concern'
|
|
2
4
|
require 'active_model/type'
|
|
3
5
|
|
|
@@ -6,6 +8,7 @@ require 'attr_json/attribute_definition/registry'
|
|
|
6
8
|
|
|
7
9
|
require 'attr_json/type/model'
|
|
8
10
|
require 'attr_json/model/cocoon_compat'
|
|
11
|
+
require 'attr_json/model/nested_model_validator'
|
|
9
12
|
|
|
10
13
|
require 'attr_json/serialization_coder_from_type'
|
|
11
14
|
|
|
@@ -20,7 +23,7 @@ module AttrJson
|
|
|
20
23
|
# Meant for use as an attribute of a AttrJson::Record. Can be nested,
|
|
21
24
|
# AttrJson::Models can have attributes that are other AttrJson::Models.
|
|
22
25
|
#
|
|
23
|
-
# @note Includes ActiveModel::Model whether you like it or not.
|
|
26
|
+
# @note Includes ActiveModel::Model whether you like it or not.
|
|
24
27
|
#
|
|
25
28
|
# You can control what happens if you set an unknown key (one that you didn't
|
|
26
29
|
# register with `attr_json`) with the config attribute `attr_json_config(unknown_key:)`.
|
|
@@ -47,6 +50,21 @@ module AttrJson
|
|
|
47
50
|
# #...
|
|
48
51
|
# end
|
|
49
52
|
#
|
|
53
|
+
# ## Date-type timezone conversion
|
|
54
|
+
#
|
|
55
|
+
# By default, AttrJson::Model date/time attributes will be
|
|
56
|
+
# [ActiveRecord timezone-aware](https://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html)
|
|
57
|
+
# based on settings of `config.active_record.time_zone_aware_attributes` and
|
|
58
|
+
# `ActiveRecord::Base.time_zone_aware_types`.
|
|
59
|
+
#
|
|
60
|
+
# If you'd like to override this, you can set:
|
|
61
|
+
#
|
|
62
|
+
# ```
|
|
63
|
+
# attr_json_config(time_zone_aware_attributes: true)
|
|
64
|
+
# attr_json_config(time_zone_aware_attributes: false)
|
|
65
|
+
# attr_json_config(time_zone_aware_attributes: [:datetime, :time]) # custom list of types
|
|
66
|
+
# ```
|
|
67
|
+
#
|
|
50
68
|
# ## ActiveRecord `serialize`
|
|
51
69
|
#
|
|
52
70
|
# If you want to map a single AttrJson::Model to a json/jsonb column, you
|
|
@@ -65,6 +83,15 @@ module AttrJson
|
|
|
65
83
|
# serialize :some_json_column, ValueModel.to_serialize_coder
|
|
66
84
|
# end
|
|
67
85
|
#
|
|
86
|
+
# # Strip nils
|
|
87
|
+
#
|
|
88
|
+
# When embedded in an `attr_json` attribute, models are normally serialized with `nil` values
|
|
89
|
+
# stripped from hash where possible, for a more compact representation.
|
|
90
|
+
# This can be set differently in the type.
|
|
91
|
+
#
|
|
92
|
+
# attr_json :lang_and_value, LangAndValue.to_type(strip_nils: false)
|
|
93
|
+
#
|
|
94
|
+
# See #serializable_hash docs for possible values.
|
|
68
95
|
module Model
|
|
69
96
|
extend ActiveSupport::Concern
|
|
70
97
|
|
|
@@ -106,14 +133,14 @@ module AttrJson
|
|
|
106
133
|
# The inverse of model#serializable_hash -- re-hydrates a serialized hash to a model.
|
|
107
134
|
#
|
|
108
135
|
# 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.
|
|
136
|
+
# like store_keys, and properly calling deserialize (rather than cast) on the underlying types.
|
|
110
137
|
#
|
|
111
138
|
# @example Model.new_from_serializable(hash)
|
|
112
139
|
def new_from_serializable(attributes = {})
|
|
113
140
|
attributes = attributes.collect do |key, value|
|
|
114
141
|
# store keys in arguments get translated to attribute names on initialize.
|
|
115
|
-
if attribute_def = self.attr_json_registry.store_key_lookup("", key
|
|
116
|
-
key = attribute_def.name
|
|
142
|
+
if attribute_def = self.attr_json_registry.store_key_lookup("".freeze, AttrJson.efficient_to_s(key))
|
|
143
|
+
key = AttrJson.efficient_to_s(attribute_def.name)
|
|
117
144
|
end
|
|
118
145
|
|
|
119
146
|
attr_type = self.attr_json_registry.has_attribute?(key) && self.attr_json_registry.type_for_attribute(key)
|
|
@@ -127,14 +154,43 @@ module AttrJson
|
|
|
127
154
|
self.new(attributes)
|
|
128
155
|
end
|
|
129
156
|
|
|
130
|
-
|
|
131
|
-
|
|
157
|
+
# @returns ActiveModel::Type suitable for including this model in
|
|
158
|
+
# an AttrJson::Record or ::Model attribute
|
|
159
|
+
#
|
|
160
|
+
# @param strip_nils [Boolean,Symbol] [true,false,:safely] as a type,
|
|
161
|
+
# should we strip nils when serializing? By default this type strips
|
|
162
|
+
# nils in :safely mode. See AttrJson::Model#serializable_hash
|
|
163
|
+
def to_type(strip_nils: :safely)
|
|
164
|
+
@type ||= AttrJson::Type::Model.new(self, strip_nils: strip_nils)
|
|
132
165
|
end
|
|
133
166
|
|
|
167
|
+
# An ActiveModel::Type that can be used to serialize this model
|
|
168
|
+
# across an entire JSON(b) column.
|
|
169
|
+
#
|
|
170
|
+
# @example using standard ActiveRecord `serialize` feature.
|
|
171
|
+
#
|
|
172
|
+
# class MyTable < ApplicationRecord
|
|
173
|
+
# serialize :some_json_column, MyModel.to_serialization_coder
|
|
174
|
+
#
|
|
175
|
+
# # In Rails 7.1+:
|
|
176
|
+
# # serialize :some_json_column, coder: MyModel.to_serialization_coder
|
|
177
|
+
# end
|
|
178
|
+
#
|
|
134
179
|
def to_serialization_coder
|
|
135
180
|
@serialization_coder ||= AttrJson::SerializationCoderFromType.new(to_type)
|
|
136
181
|
end
|
|
137
182
|
|
|
183
|
+
# like the ActiveModel::Attributes method
|
|
184
|
+
def attribute_names
|
|
185
|
+
attr_json_registry.attribute_names
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# like the ActiveModel::Attributes method, hash with name keys, and ActiveModel::Type values
|
|
189
|
+
def attribute_types
|
|
190
|
+
attribute_names.collect { |name| [AttrJson.efficient_to_s(name), attr_json_registry.type_for_attribute(name)]}.to_h
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
|
|
138
194
|
# Type can be an instance of an ActiveModel::Type::Value subclass, or a symbol that will
|
|
139
195
|
# be looked up in `ActiveModel::Type.lookup`
|
|
140
196
|
#
|
|
@@ -143,6 +199,8 @@ module AttrJson
|
|
|
143
199
|
# @param type [ActiveModel::Type::Value] An instance of an ActiveModel::Type::Value (or subclass)
|
|
144
200
|
#
|
|
145
201
|
# @option options [Boolean] :array (false) Make this attribute an array of given type.
|
|
202
|
+
# Array types default to an empty array. If you want to turn that off, you can add
|
|
203
|
+
# `default: AttrJson::AttributeDefinition::NO_DEFAULT_PROVIDED`
|
|
146
204
|
#
|
|
147
205
|
# @option options [Object] :default (nil) Default value, if a Proc object it will be #call'd
|
|
148
206
|
# for default.
|
|
@@ -150,54 +208,34 @@ module AttrJson
|
|
|
150
208
|
# @option options [String,Symbol] :store_key (nil) Serialize to JSON using
|
|
151
209
|
# given store_key, rather than name as would be usual.
|
|
152
210
|
#
|
|
153
|
-
# @option options [Boolean] :validate (true)
|
|
154
|
-
#
|
|
211
|
+
# @option options [Boolean] :validate (true) Mak validation errors on the attributes post up
|
|
212
|
+
# to self, using something similar to an ActiveRecord::Validations::AssociatedValidator
|
|
155
213
|
def attr_json(name, type, **options)
|
|
156
214
|
options.assert_valid_keys(*(AttributeDefinition::VALID_OPTIONS - [:container_attribute] + [:validate]))
|
|
157
215
|
|
|
216
|
+
type = _attr_json_maybe_wrap_timezone_aware(type)
|
|
217
|
+
|
|
158
218
|
self.attr_json_registry = attr_json_registry.with(
|
|
159
219
|
AttributeDefinition.new(name.to_sym, type, options.except(:validate))
|
|
160
220
|
)
|
|
161
221
|
|
|
162
222
|
# By default, automatically validate nested models
|
|
163
|
-
if type.kind_of?(AttrJson::Type::Model) && options[:validate] != false
|
|
164
|
-
#
|
|
165
|
-
|
|
166
|
-
self.validates_with ActiveRecord::Validations::AssociatedValidator, attributes: [name.to_sym]
|
|
223
|
+
if (type.kind_of?(AttrJson::Type::Model) || type.kind_of?(AttrJson::Type::PolymorphicModel)) && options[:validate] != false
|
|
224
|
+
# Post validations up with something based on ActiveRecord::Validations::AssociatedValidator
|
|
225
|
+
self.validates_with ::AttrJson::Model::NestedModelValidator, attributes: [name.to_sym]
|
|
167
226
|
end
|
|
168
227
|
|
|
169
228
|
_attr_jsons_module.module_eval do
|
|
170
229
|
define_method("#{name}=") do |value|
|
|
171
|
-
_attr_json_write(name
|
|
230
|
+
_attr_json_write(AttrJson.efficient_to_s(name), value)
|
|
172
231
|
end
|
|
173
232
|
|
|
174
233
|
define_method("#{name}") do
|
|
175
|
-
attributes[name
|
|
234
|
+
attributes[AttrJson.efficient_to_s(name)]
|
|
176
235
|
end
|
|
177
236
|
end
|
|
178
237
|
end
|
|
179
238
|
|
|
180
|
-
# This should kind of be considered 'protected', but the semantics
|
|
181
|
-
# of how we want to call it don't give us a visibility modifier that works.
|
|
182
|
-
# Prob means refactoring called for. TODO?
|
|
183
|
-
def fill_in_defaults(hash)
|
|
184
|
-
# Only if we need to mutate it to add defaults, we'll dup it first. deep_dup not neccesary
|
|
185
|
-
# since we're only modifying top-level here.
|
|
186
|
-
duped = false
|
|
187
|
-
attr_json_registry.definitions.each do |definition|
|
|
188
|
-
if definition.has_default? && ! (hash.has_key?(definition.store_key.to_s) || hash.has_key?(definition.store_key.to_sym))
|
|
189
|
-
unless duped
|
|
190
|
-
hash = hash.dup
|
|
191
|
-
duped = true
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
hash[definition.store_key] = definition.provide_default!
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
hash
|
|
199
|
-
end
|
|
200
|
-
|
|
201
239
|
private
|
|
202
240
|
|
|
203
241
|
# Define an anonymous module and include it, so can still be easily
|
|
@@ -210,14 +248,54 @@ module AttrJson
|
|
|
210
248
|
mod
|
|
211
249
|
end
|
|
212
250
|
end
|
|
251
|
+
|
|
252
|
+
# wrap in ActiveRecord type for timezone-awareness/conversion, if needed
|
|
253
|
+
# We at present only need this in AttrJson::Model cause in AttrJson::Record,
|
|
254
|
+
# our sync with ActiveRecord attributes takes care of it for us.
|
|
255
|
+
# See https://github.com/rails/rails/blob/v7.0.4/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb#L78
|
|
256
|
+
#
|
|
257
|
+
# For now we use ActiveRecord::Base state to decide.
|
|
258
|
+
#
|
|
259
|
+
# We have to turn a symbol type into a real object type if we want to wrap it -- we will
|
|
260
|
+
# return an actual ActiveModel::Value::Type either way, converting from symbol!
|
|
261
|
+
#
|
|
262
|
+
# That *wrapped* type will be the new type, registered with AttributeDefinition
|
|
263
|
+
# and then with Rails `attribute`, to provide timezone conversion.
|
|
264
|
+
def _attr_json_maybe_wrap_timezone_aware(type)
|
|
265
|
+
type = AttributeDefinition.lookup_type(type)
|
|
266
|
+
|
|
267
|
+
if self.attr_json_config.time_zone_aware_attributes.nil?
|
|
268
|
+
# nil config means use ActiveRecord::Base
|
|
269
|
+
aware = ActiveRecord::Base.time_zone_aware_attributes
|
|
270
|
+
aware_types = ActiveRecord::Base.time_zone_aware_types
|
|
271
|
+
elsif self.attr_json_config.time_zone_aware_attributes.kind_of?(Array)
|
|
272
|
+
# Array config means we're doing it, and these are the types
|
|
273
|
+
aware = true
|
|
274
|
+
aware_types = self.attr_json_config.time_zone_aware_attributes
|
|
275
|
+
else
|
|
276
|
+
# boolean config, types from ActiveRecord::Base
|
|
277
|
+
aware = !!self.attr_json_config.time_zone_aware_attributes
|
|
278
|
+
aware_types = ActiveRecord::Base.time_zone_aware_types
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
if aware && aware_types.include?(type.type)
|
|
282
|
+
type = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(type)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
type
|
|
286
|
+
end
|
|
213
287
|
end
|
|
214
288
|
|
|
215
289
|
def initialize(attributes = {})
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
290
|
+
super
|
|
291
|
+
|
|
292
|
+
fill_in_defaults!
|
|
293
|
+
end
|
|
219
294
|
|
|
220
|
-
|
|
295
|
+
# inspired by https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/activemodel/lib/active_model/attributes.rb
|
|
296
|
+
def initialize_dup(other) # :nodoc:
|
|
297
|
+
@attributes = @attributes.deep_dup
|
|
298
|
+
super
|
|
221
299
|
end
|
|
222
300
|
|
|
223
301
|
def attributes
|
|
@@ -255,32 +333,69 @@ module AttrJson
|
|
|
255
333
|
self.class.attr_json_registry.has_attribute?(str)
|
|
256
334
|
end
|
|
257
335
|
|
|
258
|
-
#
|
|
259
|
-
|
|
336
|
+
# like the ActiveModel::Attributes method
|
|
337
|
+
def attribute_names
|
|
338
|
+
self.class.attribute_names
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Override from ActiveModel::Serialization to:
|
|
342
|
+
#
|
|
343
|
+
# * handle store_key settings
|
|
344
|
+
#
|
|
345
|
+
# * #serialize by type to make sure any values set directly on hash still
|
|
260
346
|
# get properly type-serialized.
|
|
261
|
-
|
|
262
|
-
|
|
347
|
+
#
|
|
348
|
+
# * custom logic for keeping nil values out of serialization to be more compact
|
|
349
|
+
#
|
|
350
|
+
# @param strip_nils [:symbol, Boolean] (default false) Should we keep keys with
|
|
351
|
+
# `nil` values out of the serialization entirely? You might want to to keep
|
|
352
|
+
# your in-database serialization compact. By default this method does not -- but
|
|
353
|
+
# by default AttrJson::Type::Model sends `:safely` when serializing.
|
|
354
|
+
# * false => do not strip nils
|
|
355
|
+
# * :safely => strip nils only when there is no default value for the attribute,
|
|
356
|
+
# so `nil` can still override the default value
|
|
357
|
+
# * true => strip nils even if there is a default value -- in AttrJson
|
|
358
|
+
# context, this means the default will be reapplied over nil on
|
|
359
|
+
# every de-serialization!
|
|
360
|
+
def serializable_hash(options=nil)
|
|
361
|
+
strip_nils = options&.has_key?(:strip_nils) ? options.delete(:strip_nils) : false
|
|
362
|
+
|
|
363
|
+
unless [true, false, :safely].include?(strip_nils)
|
|
364
|
+
raise ArgumentError, ":strip_nils must be true, false, or :safely"
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
super(options).collect do |key, value|
|
|
263
368
|
if attribute_def = self.class.attr_json_registry[key.to_sym]
|
|
264
369
|
key = attribute_def.store_key
|
|
265
|
-
if value.kind_of?(Time) || value.kind_of?(DateTime)
|
|
266
|
-
value = value.utc.change(usec: 0)
|
|
267
|
-
end
|
|
268
370
|
|
|
269
371
|
value = attribute_def.serialize(value)
|
|
270
372
|
end
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
373
|
+
|
|
374
|
+
# strip_nils handling
|
|
375
|
+
if value.nil? && (
|
|
376
|
+
(strip_nils == :safely && !attribute_def&.has_default?) ||
|
|
377
|
+
strip_nils == true )
|
|
378
|
+
then
|
|
379
|
+
# do not include in serializable_hash
|
|
380
|
+
nil
|
|
381
|
+
else
|
|
382
|
+
[key, value]
|
|
383
|
+
end
|
|
384
|
+
end.compact.to_h
|
|
274
385
|
end
|
|
275
386
|
|
|
276
387
|
# ActiveRecord JSON serialization will insist on calling
|
|
277
388
|
# this, instead of the specified type's #serialize, at least in some cases.
|
|
278
389
|
# So it's important we define it -- the default #as_json added by ActiveSupport
|
|
279
390
|
# will serialize all instance variables, which is not what we want.
|
|
280
|
-
|
|
281
|
-
|
|
391
|
+
#
|
|
392
|
+
# @param strip_nils [:symbol, Boolean] (default false) [true, false, :safely],
|
|
393
|
+
# see #serializable_hash
|
|
394
|
+
def as_json(options=nil)
|
|
395
|
+
serializable_hash(options)
|
|
282
396
|
end
|
|
283
397
|
|
|
398
|
+
|
|
284
399
|
# We deep_dup on #to_h, you want attributes unduped, ask for #attributes.
|
|
285
400
|
def to_h
|
|
286
401
|
attributes.deep_dup
|
|
@@ -301,14 +416,31 @@ module AttrJson
|
|
|
301
416
|
false
|
|
302
417
|
end
|
|
303
418
|
|
|
419
|
+
# like ActiveModel::Attributes at
|
|
420
|
+
# https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/activemodel/lib/active_model/attributes.rb#L120
|
|
421
|
+
#
|
|
422
|
+
# is not a full deep freeze
|
|
423
|
+
def freeze
|
|
424
|
+
attributes.freeze unless frozen?
|
|
425
|
+
super
|
|
426
|
+
end
|
|
427
|
+
|
|
304
428
|
private
|
|
305
429
|
|
|
430
|
+
def fill_in_defaults!
|
|
431
|
+
self.class.attr_json_registry.definitions.each do |definition|
|
|
432
|
+
if definition.has_default? && !attributes.has_key?(AttrJson.efficient_to_s(definition.name))
|
|
433
|
+
self.send("#{definition.name.to_s}=", definition.provide_default!)
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
306
438
|
def _attr_json_write(key, value)
|
|
307
439
|
if attribute_def = self.class.attr_json_registry[key.to_sym]
|
|
308
|
-
attributes[key
|
|
440
|
+
attributes[AttrJson.efficient_to_s(key)] = attribute_def.cast(value)
|
|
309
441
|
else
|
|
310
442
|
# TODO, strict mode, ignore, raise, allow.
|
|
311
|
-
attributes[key
|
|
443
|
+
attributes[AttrJson.efficient_to_s(key)] = value
|
|
312
444
|
end
|
|
313
445
|
end
|
|
314
446
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'attr_json/nested_attributes/multiparameter_attribute_writer'
|
|
2
4
|
|
|
3
5
|
module AttrJson
|
|
@@ -15,12 +17,10 @@ module AttrJson
|
|
|
15
17
|
delegate :nested_attributes_options, to: :model
|
|
16
18
|
|
|
17
19
|
def assign_nested_attributes(attributes)
|
|
18
|
-
if attr_def.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
assign_nested_attributes_for_model_array(attributes)
|
|
23
|
-
end
|
|
20
|
+
if attr_def.array_of_primitive_type?
|
|
21
|
+
assign_nested_attributes_for_primitive_array(attributes)
|
|
22
|
+
elsif attr_def.array_type?
|
|
23
|
+
assign_nested_attributes_for_model_array(attributes)
|
|
24
24
|
else
|
|
25
25
|
assign_nested_attributes_for_single_model(attributes)
|
|
26
26
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'attr_json/nested_attributes/builder'
|
|
2
4
|
require 'attr_json/nested_attributes/writer'
|
|
3
5
|
|
|
@@ -65,6 +67,10 @@ module AttrJson
|
|
|
65
67
|
raise ArgumentError, "No attr_json found for name '#{attr_name}'. Has it been defined yet?"
|
|
66
68
|
end
|
|
67
69
|
|
|
70
|
+
unless attr_def.array_type? || attr_def.single_model_type?
|
|
71
|
+
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}"
|
|
72
|
+
end
|
|
73
|
+
|
|
68
74
|
# We're sharing AR class attr in an AR, or using our own in a Model.
|
|
69
75
|
nested_attributes_options = self.nested_attributes_options.dup
|
|
70
76
|
nested_attributes_options[attr_name.to_sym] = options
|
|
@@ -80,7 +86,7 @@ module AttrJson
|
|
|
80
86
|
end
|
|
81
87
|
|
|
82
88
|
# No build method for our wacky array of primitive type.
|
|
83
|
-
if options[:define_build_method] && !
|
|
89
|
+
if options[:define_build_method] && !attr_def.array_of_primitive_type?
|
|
84
90
|
_attr_jsons_module.module_eval do
|
|
85
91
|
build_method_name = "build_#{attr_name.to_s.singularize}"
|
|
86
92
|
if method_defined?(build_method_name)
|