attr_json 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +17 -0
- data/.yardopts +1 -0
- data/Gemfile +42 -0
- data/LICENSE.txt +21 -0
- data/README.md +426 -0
- data/Rakefile +8 -0
- data/bin/console +23 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/setup +11 -0
- data/config.ru +9 -0
- data/doc_src/dirty_tracking.md +155 -0
- data/doc_src/forms.md +124 -0
- data/json_attribute.gemspec +50 -0
- data/lib/attr_json.rb +18 -0
- data/lib/attr_json/attribute_definition.rb +93 -0
- data/lib/attr_json/attribute_definition/registry.rb +93 -0
- data/lib/attr_json/model.rb +270 -0
- data/lib/attr_json/model/cocoon_compat.rb +27 -0
- data/lib/attr_json/nested_attributes.rb +92 -0
- data/lib/attr_json/nested_attributes/builder.rb +24 -0
- data/lib/attr_json/nested_attributes/multiparameter_attribute_writer.rb +86 -0
- data/lib/attr_json/nested_attributes/writer.rb +215 -0
- data/lib/attr_json/record.rb +140 -0
- data/lib/attr_json/record/dirty.rb +281 -0
- data/lib/attr_json/record/query_builder.rb +84 -0
- data/lib/attr_json/record/query_scopes.rb +35 -0
- data/lib/attr_json/type/array.rb +55 -0
- data/lib/attr_json/type/container_attribute.rb +56 -0
- data/lib/attr_json/type/model.rb +77 -0
- data/lib/attr_json/version.rb +3 -0
- data/playground_models.rb +101 -0
- metadata +177 -0
data/lib/attr_json.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "attr_json/version"
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
require "active_record/connection_adapters/postgresql_adapter"
|
5
|
+
|
6
|
+
require 'attr_json/record'
|
7
|
+
require 'attr_json/model'
|
8
|
+
require 'attr_json/nested_attributes'
|
9
|
+
require 'attr_json/record/query_scopes'
|
10
|
+
|
11
|
+
# Dirty not supported on Rails 5.0
|
12
|
+
if Gem.loaded_specs["activerecord"].version.release >= Gem::Version.new('5.1')
|
13
|
+
require 'attr_json/record/dirty'
|
14
|
+
end
|
15
|
+
|
16
|
+
module AttrJson
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'attr_json/type/array'
|
2
|
+
|
3
|
+
module AttrJson
|
4
|
+
|
5
|
+
# Represents a `attr_json` definition, on either a AttrJson::Record
|
6
|
+
# or AttrJson::Model. Normally this class is only used by
|
7
|
+
# AttrJson::AttributeDefinition::{Registry}.
|
8
|
+
class AttributeDefinition
|
9
|
+
NO_DEFAULT_PROVIDED = Object.new.freeze
|
10
|
+
VALID_OPTIONS = %i{container_attribute store_key default array}.freeze
|
11
|
+
|
12
|
+
attr_reader :name, :type, :original_args, :container_attribute
|
13
|
+
|
14
|
+
# @param name [Symbol,String]
|
15
|
+
# @param type [Symbol,ActiveModel::Type::Value]
|
16
|
+
#
|
17
|
+
# @option options store_key [Symbol,String]
|
18
|
+
# @option options container_attribute [Symbol,ActiveModel::Type::Value]
|
19
|
+
# Only means something in a AttrJson::Record, no meaning in a AttrJson::Model.
|
20
|
+
# @option options default [Object,Symbol,Proc] (nil)
|
21
|
+
# @option options array [Boolean] (false)
|
22
|
+
def initialize(name, type, options = {})
|
23
|
+
options.assert_valid_keys *VALID_OPTIONS
|
24
|
+
# saving original args for reflection useful for debugging, maybe other things.
|
25
|
+
@original_args = [name, type, options]
|
26
|
+
|
27
|
+
@name = name.to_sym
|
28
|
+
|
29
|
+
@container_attribute = options[:container_attribute] && options[:container_attribute].to_s
|
30
|
+
|
31
|
+
@store_key = options[:store_key] && options[:store_key].to_s
|
32
|
+
|
33
|
+
@default = if options.has_key?(:default)
|
34
|
+
options[:default]
|
35
|
+
else
|
36
|
+
NO_DEFAULT_PROVIDED
|
37
|
+
end
|
38
|
+
|
39
|
+
if type.is_a? Symbol
|
40
|
+
# ActiveModel::Type.lookup may make more sense, but ActiveModel::Type::Date
|
41
|
+
# seems to have a bug with multi-param assignment. Mostly they return
|
42
|
+
# the same types, but ActiveRecord::Type::Date works with multi-param assignment.
|
43
|
+
type = ActiveRecord::Type.lookup(type)
|
44
|
+
elsif ! type.is_a? ActiveModel::Type::Value
|
45
|
+
raise ArgumentError, "Second argument (#{type}) must be a symbol or instance of an ActiveModel::Type::Value subclass"
|
46
|
+
end
|
47
|
+
@type = (options[:array] == true ? AttrJson::Type::Array.new(type) : type)
|
48
|
+
end
|
49
|
+
|
50
|
+
def cast(value)
|
51
|
+
type.cast(value)
|
52
|
+
end
|
53
|
+
|
54
|
+
def serialize(value)
|
55
|
+
type.serialize(value)
|
56
|
+
end
|
57
|
+
|
58
|
+
def deserialize(value)
|
59
|
+
type.deserialize(value)
|
60
|
+
end
|
61
|
+
|
62
|
+
def has_custom_store_key?
|
63
|
+
!!@store_key
|
64
|
+
end
|
65
|
+
|
66
|
+
def store_key
|
67
|
+
(@store_key || name).to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
def has_default?
|
71
|
+
@default != NO_DEFAULT_PROVIDED
|
72
|
+
end
|
73
|
+
|
74
|
+
def provide_default!
|
75
|
+
unless has_default?
|
76
|
+
raise ArgumentError.new("This #{self.class.name} does not have a default defined!")
|
77
|
+
end
|
78
|
+
|
79
|
+
# Seems weird to assume a Proc can't be the default itself, but I guess
|
80
|
+
# Proc's aren't serializable, so fine assumption. Modeled after:
|
81
|
+
# https://github.com/rails/rails/blob/f2dfd5c6fdffdf65e6f07aae8e855ac802f9302f/activerecord/lib/active_record/attribute/user_provided_default.rb#L12-L16
|
82
|
+
if @default.is_a?(Proc)
|
83
|
+
cast(@default.call)
|
84
|
+
else
|
85
|
+
cast(@default)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def array_type?
|
90
|
+
type.is_a? AttrJson::Type::Array
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'attr_json/attribute_definition'
|
2
|
+
|
3
|
+
module AttrJson
|
4
|
+
class AttributeDefinition
|
5
|
+
# Attached to a class to record the json attributes registered,
|
6
|
+
# with either AttrJson::Record or AttrJson::Model.
|
7
|
+
#
|
8
|
+
# Think of it as mostly like a hash keyed by attribute name, value
|
9
|
+
# an AttributeDefinition.
|
10
|
+
#
|
11
|
+
# It is expected to be used by AttrJson::Record and AttrJson::Model,
|
12
|
+
# you shouldn't need to interact with it directly.
|
13
|
+
#
|
14
|
+
# It is intentionally immutable to make it harder to accidentally mutate
|
15
|
+
# a registry shared with superclass in a `class_attribute`, instead of
|
16
|
+
# properly assigning a new modified registry.
|
17
|
+
#
|
18
|
+
# self.some_registry_attribute = self.some_registry_attribute.with(
|
19
|
+
# attr_definition_1, attr_definition_2
|
20
|
+
# )
|
21
|
+
# # => Returns a NEW AttributeDefinition object
|
22
|
+
#
|
23
|
+
# All references in code to "definition" are to a AttrJson::AttributeDefinition instance.
|
24
|
+
class Registry
|
25
|
+
def initialize(hash = {})
|
26
|
+
@name_to_definition = hash
|
27
|
+
@store_key_to_definition = {}
|
28
|
+
definitions.each { |d| store_key_index!(d) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def fetch(key, *args, &block)
|
32
|
+
@name_to_definition.fetch(key.to_sym, *args, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
def [](key)
|
36
|
+
@name_to_definition[key.to_sym]
|
37
|
+
end
|
38
|
+
|
39
|
+
def has_attribute?(key)
|
40
|
+
@name_to_definition.has_key?(key.to_sym)
|
41
|
+
end
|
42
|
+
|
43
|
+
def type_for_attribute(key)
|
44
|
+
self[key].type
|
45
|
+
end
|
46
|
+
|
47
|
+
# Can return nil if none found.
|
48
|
+
def store_key_lookup(container_attribute, store_key)
|
49
|
+
@store_key_to_definition[container_attribute.to_s] &&
|
50
|
+
@store_key_to_definition[container_attribute.to_s][store_key.to_s]
|
51
|
+
end
|
52
|
+
|
53
|
+
def definitions
|
54
|
+
@name_to_definition.values
|
55
|
+
end
|
56
|
+
|
57
|
+
def container_attributes
|
58
|
+
@store_key_to_definition.keys.collect(&:to_s)
|
59
|
+
end
|
60
|
+
|
61
|
+
# This is how you register additional definitions, as a non-mutating
|
62
|
+
# return-a-copy operation.
|
63
|
+
def with(*definitions)
|
64
|
+
self.class.new(@name_to_definition).tap do |copied|
|
65
|
+
definitions.each do |defin|
|
66
|
+
copied.add!(defin)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
def add!(definition)
|
74
|
+
if @name_to_definition.has_key?(definition.name)
|
75
|
+
raise ArgumentError, "Can't add, conflict with existing attribute name `#{definition.name.to_sym}`: #{@name_to_definition[definition.name].original_args}"
|
76
|
+
end
|
77
|
+
@name_to_definition[definition.name.to_sym] = definition
|
78
|
+
store_key_index!(definition)
|
79
|
+
end
|
80
|
+
|
81
|
+
def store_key_index!(definition)
|
82
|
+
container_hash = (@store_key_to_definition[definition.container_attribute.to_s] ||= {})
|
83
|
+
|
84
|
+
if container_hash.has_key?(definition.store_key.to_s)
|
85
|
+
existing = container_hash[definition.store_key.to_s]
|
86
|
+
raise ArgumentError, "Can't add, store key `#{definition.store_key}` conflicts with existing attribute: #{existing.original_args}"
|
87
|
+
end
|
88
|
+
|
89
|
+
container_hash[definition.store_key.to_s] = definition
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,270 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_model/type'
|
3
|
+
|
4
|
+
require 'attr_json/attribute_definition'
|
5
|
+
require 'attr_json/attribute_definition/registry'
|
6
|
+
|
7
|
+
require 'attr_json/type/model'
|
8
|
+
require 'attr_json/model/cocoon_compat'
|
9
|
+
|
10
|
+
module AttrJson
|
11
|
+
|
12
|
+
# Meant for use in a plain class, turns it into an ActiveModel::Model
|
13
|
+
# with attr_json support. NOT for use in an ActiveRecord::Base model,
|
14
|
+
# see `Record` for ActiveRecord use.
|
15
|
+
#
|
16
|
+
# Creates an ActiveModel object with _typed_ attributes, easily serializable
|
17
|
+
# to json, and with a corresponding ActiveModel::Type representing the class.
|
18
|
+
# Meant for use as an attribute of a AttrJson::Record. Can be nested,
|
19
|
+
# AttrJson::Models can have attributes that are other AttrJson::Models.
|
20
|
+
#
|
21
|
+
# @note Includes ActiveModel::Model whether you like it or not. TODO, should it?
|
22
|
+
#
|
23
|
+
# You can control what happens if you set an unknown key (one that you didn't
|
24
|
+
# register with `attr_json`) with the class attribute `attr_json_unknown_key`.
|
25
|
+
# * :raise (default) raise ActiveModel::UnknownAttributeError
|
26
|
+
# * :strip Ignore the unknown key and do not include it, without raising.
|
27
|
+
# * :allow Allow the unknown key and it's value to be in the serialized hash,
|
28
|
+
# and written to the database. May be useful for legacy data or columns
|
29
|
+
# that other software touches, to let unknown keys just flow through.
|
30
|
+
module Model
|
31
|
+
extend ActiveSupport::Concern
|
32
|
+
|
33
|
+
include ActiveModel::Model
|
34
|
+
include ActiveModel::Serialization
|
35
|
+
#include ActiveModel::Dirty
|
36
|
+
|
37
|
+
included do
|
38
|
+
if self < ActiveRecord::Base
|
39
|
+
raise TypeError, "AttrJson::Model is not for an ActiveRecord::Base model. #{self} appears to be one. Are you looking for ::AttrJson::Record?"
|
40
|
+
end
|
41
|
+
|
42
|
+
class_attribute :attr_json_registry, instance_accessor: false
|
43
|
+
self.attr_json_registry = ::AttrJson::AttributeDefinition::Registry.new
|
44
|
+
|
45
|
+
# :raise, :strip, :allow. :raise is default. Is there some way to enforce this.
|
46
|
+
class_attribute :attr_json_unknown_key
|
47
|
+
self.attr_json_unknown_key ||= :raise
|
48
|
+
end
|
49
|
+
|
50
|
+
class_methods do
|
51
|
+
# Like `.new`, but translate store keys in hash
|
52
|
+
def new_from_serializable(attributes = {})
|
53
|
+
attributes = attributes.transform_keys do |key|
|
54
|
+
# store keys in arguments get translated to attribute names on initialize.
|
55
|
+
if attribute_def = self.attr_json_registry.store_key_lookup("", key.to_s)
|
56
|
+
attribute_def.name.to_s
|
57
|
+
else
|
58
|
+
key
|
59
|
+
end
|
60
|
+
end
|
61
|
+
self.new(attributes)
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_type
|
65
|
+
@type ||= AttrJson::Type::Model.new(self)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Type can be an instance of an ActiveModel::Type::Value subclass, or a symbol that will
|
69
|
+
# be looked up in `ActiveModel::Type.lookup`
|
70
|
+
#
|
71
|
+
# @param name [Symbol,String] name of attribute
|
72
|
+
#
|
73
|
+
# @param type [ActiveModel::Type::Value] An instance of an ActiveModel::Type::Value (or subclass)
|
74
|
+
#
|
75
|
+
# @option options [Boolean] :array (false) Make this attribute an array of given type.
|
76
|
+
#
|
77
|
+
# @option options [Object] :default (nil) Default value, if a Proc object it will be #call'd
|
78
|
+
# for default.
|
79
|
+
#
|
80
|
+
# @option options [String,Symbol] :store_key (nil) Serialize to JSON using
|
81
|
+
# given store_key, rather than name as would be usual.
|
82
|
+
#
|
83
|
+
# @option options [Boolean] :validate (true) Create an ActiveRecord::Validations::AssociatedValidator so
|
84
|
+
# validation errors on the attributes post up to self.
|
85
|
+
def attr_json(name, type, **options)
|
86
|
+
options.assert_valid_keys(*(AttributeDefinition::VALID_OPTIONS - [:container_attribute] + [:validate]))
|
87
|
+
|
88
|
+
self.attr_json_registry = attr_json_registry.with(
|
89
|
+
AttributeDefinition.new(name.to_sym, type, options.except(:validate))
|
90
|
+
)
|
91
|
+
|
92
|
+
# By default, automatically validate nested models
|
93
|
+
if type.kind_of?(AttrJson::Type::Model) && options[:validate] != false
|
94
|
+
# Yes. we're passing an ActiveRecord::Validations validator, but
|
95
|
+
# it works fine for ActiveModel. If this changes in the future, tests will catch.
|
96
|
+
self.validates_with ActiveRecord::Validations::AssociatedValidator, attributes: [name.to_sym]
|
97
|
+
end
|
98
|
+
|
99
|
+
_attr_jsons_module.module_eval do
|
100
|
+
define_method("#{name}=") do |value|
|
101
|
+
_attr_json_write(name.to_s, value)
|
102
|
+
end
|
103
|
+
|
104
|
+
define_method("#{name}") do
|
105
|
+
attributes[name.to_s]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# This should kind of be considered 'protected', but the semantics
|
111
|
+
# of how we want to call it don't give us a visibility modifier that works.
|
112
|
+
# Prob means refactoring called for. TODO?
|
113
|
+
def fill_in_defaults(hash)
|
114
|
+
# Only if we need to mutate it to add defaults, we'll dup it first. deep_dup not neccesary
|
115
|
+
# since we're only modifying top-level here.
|
116
|
+
duped = false
|
117
|
+
attr_json_registry.definitions.each do |definition|
|
118
|
+
if definition.has_default? && ! (hash.has_key?(definition.store_key.to_s) || hash.has_key?(definition.store_key.to_sym))
|
119
|
+
unless duped
|
120
|
+
hash = hash.dup
|
121
|
+
duped = true
|
122
|
+
end
|
123
|
+
|
124
|
+
hash[definition.store_key] = definition.provide_default!
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
hash
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
# Define an anonymous module and include it, so can still be easily
|
134
|
+
# overridden by concrete class. Design cribbed from ActiveRecord::Store
|
135
|
+
# https://github.com/rails/rails/blob/4590d7729e241cb7f66e018a2a9759cb3baa36e5/activerecord/lib/active_record/store.rb
|
136
|
+
def _attr_jsons_module # :nodoc:
|
137
|
+
@_attr_jsons_module ||= begin
|
138
|
+
mod = Module.new
|
139
|
+
include mod
|
140
|
+
mod
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def initialize(attributes = {})
|
146
|
+
if !attributes.respond_to?(:transform_keys)
|
147
|
+
raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
|
148
|
+
end
|
149
|
+
|
150
|
+
super(self.class.fill_in_defaults(attributes))
|
151
|
+
end
|
152
|
+
|
153
|
+
def attributes
|
154
|
+
@attributes ||= {}
|
155
|
+
end
|
156
|
+
|
157
|
+
# ActiveModel method, called in initialize. overridden.
|
158
|
+
# from https://github.com/rails/rails/blob/42a16a4d6514f28e05f1c22a5f9125d194d9c7cb/activemodel/lib/active_model/attribute_assignment.rb
|
159
|
+
def assign_attributes(new_attributes)
|
160
|
+
if !new_attributes.respond_to?(:stringify_keys)
|
161
|
+
raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
|
162
|
+
end
|
163
|
+
return if new_attributes.empty?
|
164
|
+
|
165
|
+
# stringify keys just like https://github.com/rails/rails/blob/4f99a2186479d5f77460622f2c0f37708b3ec1bc/activemodel/lib/active_model/attribute_assignment.rb#L34
|
166
|
+
new_attributes.stringify_keys.each do |k, v|
|
167
|
+
setter = :"#{k}="
|
168
|
+
if respond_to?(setter)
|
169
|
+
public_send(setter, v)
|
170
|
+
else
|
171
|
+
_attr_json_write_unknown_attribute(k, v)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# This attribute from ActiveRecord makes SimpleForm happy, and able to detect
|
177
|
+
# type.
|
178
|
+
def type_for_attribute(attr_name)
|
179
|
+
self.class.attr_json_registry.type_for_attribute(attr_name)
|
180
|
+
end
|
181
|
+
|
182
|
+
# This attribute from ActiveRecord make SimpleForm happy, and able to detect
|
183
|
+
# type.
|
184
|
+
def has_attribute?(str)
|
185
|
+
self.class.attr_json_registry.has_attribute?(str)
|
186
|
+
end
|
187
|
+
|
188
|
+
# Override from ActiveModel::Serialization to #serialize
|
189
|
+
# by type to make sure any values set directly on hash still
|
190
|
+
# get properly type-serialized.
|
191
|
+
def serializable_hash(*options)
|
192
|
+
super.collect do |key, value|
|
193
|
+
if attribute_def = self.class.attr_json_registry[key.to_sym]
|
194
|
+
key = attribute_def.store_key
|
195
|
+
if value.kind_of?(Time) || value.kind_of?(DateTime)
|
196
|
+
value = value.utc.change(usec: 0)
|
197
|
+
end
|
198
|
+
|
199
|
+
value = attribute_def.serialize(value)
|
200
|
+
end
|
201
|
+
# Do we need unknown key handling here? Apparently not?
|
202
|
+
[key, value]
|
203
|
+
end.to_h
|
204
|
+
end
|
205
|
+
|
206
|
+
# ActiveRecord JSON serialization will insist on calling
|
207
|
+
# this, instead of the specified type's #serialize, at least in some cases.
|
208
|
+
# So it's important we define it -- the default #as_json added by ActiveSupport
|
209
|
+
# will serialize all instance variables, which is not what we want.
|
210
|
+
def as_json(*options)
|
211
|
+
serializable_hash(*options)
|
212
|
+
end
|
213
|
+
|
214
|
+
# We deep_dup on #to_h, you want attributes unduped, ask for #attributes.
|
215
|
+
def to_h
|
216
|
+
attributes.deep_dup
|
217
|
+
end
|
218
|
+
|
219
|
+
# Two AttrJson::Model objects are equal if they are the same class
|
220
|
+
# or one is a subclass of the other, AND their #attributes are equal.
|
221
|
+
# TODO: Should we allow subclasses to be equal, or should they have to be the
|
222
|
+
# exact same class?
|
223
|
+
def ==(other_object)
|
224
|
+
(other_object.is_a?(self.class) || self.is_a?(other_object.class)) &&
|
225
|
+
other_object.attributes == self.attributes
|
226
|
+
end
|
227
|
+
|
228
|
+
# ActiveRecord objects [have a](https://github.com/rails/rails/blob/v5.1.5/activerecord/lib/active_record/nested_attributes.rb#L367-L374)
|
229
|
+
# `_destroy`, related to `marked_for_destruction?` functionality used with AR nested attributes.
|
230
|
+
# We don't mark for destruction, our nested attributes implementation just deletes immediately,
|
231
|
+
# but having this simple method always returning false makes things work more compatibly
|
232
|
+
# and smoothly with standard code for nested attributes deletion in form builders.
|
233
|
+
def _destroy
|
234
|
+
false
|
235
|
+
end
|
236
|
+
|
237
|
+
private
|
238
|
+
|
239
|
+
def _attr_json_write(key, value)
|
240
|
+
if attribute_def = self.class.attr_json_registry[key.to_sym]
|
241
|
+
attributes[key.to_s] = attribute_def.cast(value)
|
242
|
+
else
|
243
|
+
# TODO, strict mode, ignore, raise, allow.
|
244
|
+
attributes[key.to_s] = value
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
|
249
|
+
def _attr_json_write_unknown_attribute(key, value)
|
250
|
+
case attr_json_unknown_key
|
251
|
+
when :strip
|
252
|
+
# drop it, no-op
|
253
|
+
when :allow
|
254
|
+
# just put it in the hash and let standard JSON casting have it
|
255
|
+
_attr_json_write(key, value)
|
256
|
+
else
|
257
|
+
# default, :raise
|
258
|
+
raise ActiveModel::UnknownAttributeError.new(self, key)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# ActiveModel override.
|
263
|
+
# Don't take from instance variables, take from the attributes
|
264
|
+
# hash itself. Docs suggest we can override this for this very
|
265
|
+
# use case: https://github.com/rails/rails/blob/e1e3be7c02acb0facbf81a97bbfe6d1a6e9ca598/activemodel/lib/active_model/serialization.rb#L152-L168
|
266
|
+
def read_attribute_for_serialization(key)
|
267
|
+
attributes[key]
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|