attr_json 0.7.0 → 1.5.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 +87 -0
- data/.github/workflows/future_rails_ci.yml +66 -0
- data/Appraisals +26 -11
- data/CHANGELOG.md +67 -0
- data/Gemfile +15 -8
- data/README.md +123 -34
- data/attr_json.gemspec +20 -4
- data/gemfiles/rails_5_0.gemfile +4 -3
- data/gemfiles/rails_5_1.gemfile +4 -3
- data/gemfiles/rails_5_2.gemfile +4 -3
- data/gemfiles/rails_6_0.gemfile +6 -5
- data/gemfiles/rails_6_1.gemfile +19 -0
- data/gemfiles/rails_7_0.gemfile +19 -0
- data/gemfiles/{rails_edge_6.gemfile → rails_edge.gemfile} +6 -6
- data/lib/attr_json/attribute_definition.rb +6 -0
- data/lib/attr_json/config.rb +12 -2
- data/lib/attr_json/model.rb +97 -37
- data/lib/attr_json/record/dirty.rb +7 -1
- data/lib/attr_json/record/query_builder.rb +15 -3
- data/lib/attr_json/record/query_scopes.rb +6 -0
- data/lib/attr_json/record.rb +20 -4
- data/lib/attr_json/serialization_coder_from_type.rb +40 -0
- data/lib/attr_json/type/model.rb +35 -8
- data/lib/attr_json/type/polymorphic_model.rb +7 -0
- data/lib/attr_json/version.rb +1 -1
- data/lib/attr_json.rb +0 -1
- metadata +40 -22
- data/.travis.yml +0 -28
data/gemfiles/rails_6_0.gemfile
CHANGED
@@ -2,16 +2,17 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
-
gem "combustion",
|
6
|
-
gem "rails", ">= 6.0.0
|
7
|
-
gem "railties"
|
5
|
+
gem "combustion", "~> 1.0"
|
6
|
+
gem "rails", ">= 6.0.0", "< 6.1"
|
8
7
|
gem "pg", "~> 1.0"
|
9
|
-
gem "rspec-rails", "~>
|
8
|
+
gem "rspec-rails", "~> 4.0"
|
10
9
|
gem "simple_form", ">= 4.0"
|
11
10
|
gem "cocoon", ">= 1.2"
|
12
11
|
gem "jquery-rails"
|
12
|
+
gem "coffee-rails"
|
13
|
+
gem "sprockets-rails"
|
13
14
|
gem "capybara", "~> 3.0"
|
14
|
-
gem "webdrivers", "~>
|
15
|
+
gem "webdrivers", "~> 4.0"
|
15
16
|
gem "selenium-webdriver"
|
16
17
|
gem "byebug"
|
17
18
|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "combustion", "~> 1.0"
|
6
|
+
gem "rails", "~> 6.1.0"
|
7
|
+
gem "pg", "~> 1.0"
|
8
|
+
gem "rspec-rails", "~> 4.0"
|
9
|
+
gem "simple_form", ">= 4.0"
|
10
|
+
gem "cocoon", ">= 1.2"
|
11
|
+
gem "jquery-rails"
|
12
|
+
gem "coffee-rails"
|
13
|
+
gem "sprockets-rails"
|
14
|
+
gem "capybara", "~> 3.0"
|
15
|
+
gem "webdrivers", "~> 4.0"
|
16
|
+
gem "selenium-webdriver"
|
17
|
+
gem "byebug"
|
18
|
+
|
19
|
+
gemspec path: "../"
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "combustion", "~> 1.0"
|
6
|
+
gem "rails", "~> 7.0.0"
|
7
|
+
gem "pg", "~> 1.0"
|
8
|
+
gem "rspec-rails", "~> 4.0"
|
9
|
+
gem "simple_form", ">= 4.0"
|
10
|
+
gem "cocoon", ">= 1.2"
|
11
|
+
gem "jquery-rails"
|
12
|
+
gem "coffee-rails"
|
13
|
+
gem "sprockets-rails"
|
14
|
+
gem "capybara", "~> 3.0"
|
15
|
+
gem "webdrivers", "~> 4.0"
|
16
|
+
gem "selenium-webdriver"
|
17
|
+
gem "byebug"
|
18
|
+
|
19
|
+
gemspec path: "../"
|
@@ -2,18 +2,18 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
-
gem "combustion",
|
6
|
-
gem "rails", git: "https://github.com/rails/rails.git", branch: "
|
7
|
-
gem "railties"
|
5
|
+
gem "combustion", "~> 1.0", github: "pat/combustion"
|
6
|
+
gem "rails", git: "https://github.com/rails/rails.git", branch: "main"
|
8
7
|
gem "pg", "~> 1.0"
|
9
|
-
gem "rspec-rails", "~>
|
8
|
+
gem "rspec-rails", "~> 4.0"
|
10
9
|
gem "simple_form", ">= 4.0"
|
11
10
|
gem "cocoon", ">= 1.2"
|
12
11
|
gem "jquery-rails"
|
12
|
+
gem "coffee-rails"
|
13
|
+
gem "sprockets-rails"
|
13
14
|
gem "capybara", "~> 3.0"
|
14
|
-
gem "webdrivers", "~>
|
15
|
+
gem "webdrivers", "~> 4.0"
|
15
16
|
gem "selenium-webdriver"
|
16
17
|
gem "byebug"
|
17
|
-
gem "coffee-rails"
|
18
18
|
|
19
19
|
gemspec path: "../"
|
@@ -78,6 +78,12 @@
|
|
78
78
|
@default != NO_DEFAULT_PROVIDED
|
79
79
|
end
|
80
80
|
|
81
|
+
# Can be value or proc!
|
82
|
+
def default_argument
|
83
|
+
return nil unless has_default?
|
84
|
+
@default
|
85
|
+
end
|
86
|
+
|
81
87
|
def provide_default!
|
82
88
|
unless has_default?
|
83
89
|
raise ArgumentError.new("This #{self.class.name} does not have a default defined!")
|
data/lib/attr_json/config.rb
CHANGED
@@ -3,10 +3,20 @@ module AttrJson
|
|
3
3
|
# and rails class_attribute. Instead, you set to new Config object
|
4
4
|
# changed with {#merge}.
|
5
5
|
class Config
|
6
|
-
RECORD_ALLOWED_KEYS = %i{
|
7
|
-
|
6
|
+
RECORD_ALLOWED_KEYS = %i{
|
7
|
+
default_container_attribute
|
8
|
+
default_rails_attribute
|
9
|
+
default_accepts_nested_attributes
|
10
|
+
}
|
11
|
+
|
12
|
+
MODEL_ALLOWED_KEYS = %i{
|
13
|
+
unknown_key
|
14
|
+
bad_cast
|
15
|
+
}
|
16
|
+
|
8
17
|
DEFAULTS = {
|
9
18
|
default_container_attribute: "json_attributes",
|
19
|
+
default_rails_attribute: false,
|
10
20
|
unknown_key: :raise
|
11
21
|
}
|
12
22
|
|
data/lib/attr_json/model.rb
CHANGED
@@ -7,6 +7,8 @@ require 'attr_json/attribute_definition/registry'
|
|
7
7
|
require 'attr_json/type/model'
|
8
8
|
require 'attr_json/model/cocoon_compat'
|
9
9
|
|
10
|
+
require 'attr_json/serialization_coder_from_type'
|
11
|
+
|
10
12
|
module AttrJson
|
11
13
|
|
12
14
|
# Meant for use in a plain class, turns it into an ActiveModel::Model
|
@@ -30,9 +32,39 @@ module AttrJson
|
|
30
32
|
#
|
31
33
|
# class Something
|
32
34
|
# include AttrJson::Model
|
33
|
-
# attr_json_config(unknown_key: :
|
35
|
+
# attr_json_config(unknown_key: :allow)
|
36
|
+
# #...
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# Similarly, trying to set a Model-valued attribute with an object that
|
40
|
+
# can't be cast to a Hash or Model at all will normally raise a
|
41
|
+
# AttrJson::Type::Model::BadCast error, but you can set config `bad_cast: :as_nil`
|
42
|
+
# to make it cast to nil, more like typical ActiveRecord cast.
|
43
|
+
#
|
44
|
+
# class Something
|
45
|
+
# include AttrJson::Model
|
46
|
+
# attr_json_config(bad_cast: :as_nil)
|
34
47
|
# #...
|
35
48
|
# end
|
49
|
+
#
|
50
|
+
# ## ActiveRecord `serialize`
|
51
|
+
#
|
52
|
+
# If you want to map a single AttrJson::Model to a json/jsonb column, you
|
53
|
+
# can use ActiveRecord `serialize` feature.
|
54
|
+
#
|
55
|
+
# https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html
|
56
|
+
#
|
57
|
+
# We provide a simple shim to give you the right API for a "coder" for AR serialize:
|
58
|
+
#
|
59
|
+
# class ValueModel
|
60
|
+
# include AttrJson::Model
|
61
|
+
# attr_json :some_string, :string
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# class SomeModel < ApplicationRecord
|
65
|
+
# serialize :some_json_column, ValueModel.to_serialize_coder
|
66
|
+
# end
|
67
|
+
#
|
36
68
|
module Model
|
37
69
|
extend ActiveSupport::Concern
|
38
70
|
|
@@ -71,16 +103,27 @@ module AttrJson
|
|
71
103
|
end
|
72
104
|
|
73
105
|
|
74
|
-
#
|
106
|
+
# The inverse of model#serializable_hash -- re-hydrates a serialized hash to a model.
|
107
|
+
#
|
108
|
+
# 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.
|
110
|
+
#
|
111
|
+
# @example Model.new_from_serializable(hash)
|
75
112
|
def new_from_serializable(attributes = {})
|
76
|
-
attributes = attributes.
|
113
|
+
attributes = attributes.collect do |key, value|
|
77
114
|
# store keys in arguments get translated to attribute names on initialize.
|
78
115
|
if attribute_def = self.attr_json_registry.store_key_lookup("", key.to_s)
|
79
|
-
attribute_def.name.to_s
|
80
|
-
else
|
81
|
-
key
|
116
|
+
key = attribute_def.name.to_s
|
82
117
|
end
|
83
|
-
|
118
|
+
|
119
|
+
attr_type = self.attr_json_registry.has_attribute?(key) && self.attr_json_registry.type_for_attribute(key)
|
120
|
+
if attr_type
|
121
|
+
value = attr_type.deserialize(value)
|
122
|
+
end
|
123
|
+
|
124
|
+
[key, value]
|
125
|
+
end.to_h
|
126
|
+
|
84
127
|
self.new(attributes)
|
85
128
|
end
|
86
129
|
|
@@ -88,6 +131,21 @@ module AttrJson
|
|
88
131
|
@type ||= AttrJson::Type::Model.new(self)
|
89
132
|
end
|
90
133
|
|
134
|
+
def to_serialization_coder
|
135
|
+
@serialization_coder ||= AttrJson::SerializationCoderFromType.new(to_type)
|
136
|
+
end
|
137
|
+
|
138
|
+
# like the ActiveModel::Attributes method
|
139
|
+
def attribute_names
|
140
|
+
attr_json_registry.attribute_names
|
141
|
+
end
|
142
|
+
|
143
|
+
# like the ActiveModel::Attributes method, hash with name keys, and ActiveModel::Type values
|
144
|
+
def attribute_types
|
145
|
+
attribute_names.collect { |name| [name.to_s, attr_json_registry.type_for_attribute(name)]}.to_h
|
146
|
+
end
|
147
|
+
|
148
|
+
|
91
149
|
# Type can be an instance of an ActiveModel::Type::Value subclass, or a symbol that will
|
92
150
|
# be looked up in `ActiveModel::Type.lookup`
|
93
151
|
#
|
@@ -130,27 +188,6 @@ module AttrJson
|
|
130
188
|
end
|
131
189
|
end
|
132
190
|
|
133
|
-
# This should kind of be considered 'protected', but the semantics
|
134
|
-
# of how we want to call it don't give us a visibility modifier that works.
|
135
|
-
# Prob means refactoring called for. TODO?
|
136
|
-
def fill_in_defaults(hash)
|
137
|
-
# Only if we need to mutate it to add defaults, we'll dup it first. deep_dup not neccesary
|
138
|
-
# since we're only modifying top-level here.
|
139
|
-
duped = false
|
140
|
-
attr_json_registry.definitions.each do |definition|
|
141
|
-
if definition.has_default? && ! (hash.has_key?(definition.store_key.to_s) || hash.has_key?(definition.store_key.to_sym))
|
142
|
-
unless duped
|
143
|
-
hash = hash.dup
|
144
|
-
duped = true
|
145
|
-
end
|
146
|
-
|
147
|
-
hash[definition.store_key] = definition.provide_default!
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
hash
|
152
|
-
end
|
153
|
-
|
154
191
|
private
|
155
192
|
|
156
193
|
# Define an anonymous module and include it, so can still be easily
|
@@ -166,11 +203,15 @@ module AttrJson
|
|
166
203
|
end
|
167
204
|
|
168
205
|
def initialize(attributes = {})
|
169
|
-
|
170
|
-
|
171
|
-
|
206
|
+
super
|
207
|
+
|
208
|
+
fill_in_defaults!
|
209
|
+
end
|
172
210
|
|
173
|
-
|
211
|
+
# inspired by https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/activemodel/lib/active_model/attributes.rb
|
212
|
+
def initialize_dup(other) # :nodoc:
|
213
|
+
@attributes = @attributes.deep_dup
|
214
|
+
super
|
174
215
|
end
|
175
216
|
|
176
217
|
def attributes
|
@@ -208,6 +249,11 @@ module AttrJson
|
|
208
249
|
self.class.attr_json_registry.has_attribute?(str)
|
209
250
|
end
|
210
251
|
|
252
|
+
# like the ActiveModel::Attributes method
|
253
|
+
def attribute_names
|
254
|
+
self.class.attribute_names
|
255
|
+
end
|
256
|
+
|
211
257
|
# Override from ActiveModel::Serialization to #serialize
|
212
258
|
# by type to make sure any values set directly on hash still
|
213
259
|
# get properly type-serialized.
|
@@ -240,12 +286,9 @@ module AttrJson
|
|
240
286
|
end
|
241
287
|
|
242
288
|
# Two AttrJson::Model objects are equal if they are the same class
|
243
|
-
#
|
244
|
-
# TODO: Should we allow subclasses to be equal, or should they have to be the
|
245
|
-
# exact same class?
|
289
|
+
# AND their #attributes are equal.
|
246
290
|
def ==(other_object)
|
247
|
-
|
248
|
-
other_object.attributes == self.attributes
|
291
|
+
other_object.class == self.class && other_object.attributes == self.attributes
|
249
292
|
end
|
250
293
|
|
251
294
|
# ActiveRecord objects [have a](https://github.com/rails/rails/blob/v5.1.5/activerecord/lib/active_record/nested_attributes.rb#L367-L374)
|
@@ -257,8 +300,25 @@ module AttrJson
|
|
257
300
|
false
|
258
301
|
end
|
259
302
|
|
303
|
+
# like ActiveModel::Attributes at
|
304
|
+
# https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/activemodel/lib/active_model/attributes.rb#L120
|
305
|
+
#
|
306
|
+
# is not a full deep freeze
|
307
|
+
def freeze
|
308
|
+
attributes.freeze unless frozen?
|
309
|
+
super
|
310
|
+
end
|
311
|
+
|
260
312
|
private
|
261
313
|
|
314
|
+
def fill_in_defaults!
|
315
|
+
self.class.attr_json_registry.definitions.each do |definition|
|
316
|
+
if definition.has_default? && !attributes.has_key?(definition.name.to_s)
|
317
|
+
self.send("#{definition.name.to_s}=", definition.provide_default!)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
262
322
|
def _attr_json_write(key, value)
|
263
323
|
if attribute_def = self.class.attr_json_registry[key.to_sym]
|
264
324
|
attributes[key.to_s] = attribute_def.cast(value)
|
@@ -270,7 +270,13 @@ module AttrJson
|
|
270
270
|
# find it from currently declared attributes.
|
271
271
|
# https://github.com/rails/rails/blob/6aa5cf03ea8232180ffbbae4c130b051f813c670/activemodel/lib/active_model/attribute_methods.rb#L463-L468
|
272
272
|
def matched_attribute_method(method_name)
|
273
|
-
|
273
|
+
if self.class.respond_to?(:attribute_method_patterns_matching, true)
|
274
|
+
# Rails 7.1+
|
275
|
+
matches = self.class.send(:attribute_method_patterns_matching, method_name)
|
276
|
+
else
|
277
|
+
matches = self.class.send(:attribute_method_matchers_matching, method_name)
|
278
|
+
end
|
279
|
+
|
274
280
|
matches.detect do |match|
|
275
281
|
registry.has_attribute?(match.attr_name)
|
276
282
|
end
|
@@ -10,6 +10,20 @@ module AttrJson
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def contains_relation
|
13
|
+
contains_relation_impl do |relation, query, params|
|
14
|
+
relation.where(query, params)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def contains_not_relation
|
19
|
+
contains_relation_impl do |relation, query, params|
|
20
|
+
relation.where.not(query, params)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def contains_relation_impl
|
13
27
|
result_relation = relation
|
14
28
|
|
15
29
|
group_attributes_by_container.each do |container_attribute, attributes|
|
@@ -18,14 +32,12 @@ module AttrJson
|
|
18
32
|
attributes.each do |key, value|
|
19
33
|
add_to_param_hash!(param_hash, key, value)
|
20
34
|
end
|
21
|
-
result_relation = result_relation
|
35
|
+
result_relation = yield(result_relation, "#{relation.table_name}.#{container_attribute} @> (?)::jsonb", param_hash.to_json)
|
22
36
|
end
|
23
37
|
|
24
38
|
result_relation
|
25
39
|
end
|
26
40
|
|
27
|
-
protected
|
28
|
-
|
29
41
|
def merge_param_hash!(original, new)
|
30
42
|
original.deep_merge!(new) do |key, old_val, new_val|
|
31
43
|
if old_val.is_a?(Array) && old_val.first.is_a?(Hash) && new_val.is_a?(Array) && new_val.first.is_a?(Hash)
|
@@ -17,6 +17,8 @@ module AttrJson
|
|
17
17
|
#
|
18
18
|
# some_model.jsonb_contains(a_string: "foo").first
|
19
19
|
#
|
20
|
+
# some_model.not_jsonb_contains(a_string: "bar").first
|
21
|
+
#
|
20
22
|
# See more in {file:README} docs.
|
21
23
|
module QueryScopes
|
22
24
|
extend ActiveSupport::Concern
|
@@ -29,6 +31,10 @@ module AttrJson
|
|
29
31
|
scope(:jsonb_contains, lambda do |attributes|
|
30
32
|
QueryBuilder.new(self, attributes).contains_relation
|
31
33
|
end)
|
34
|
+
|
35
|
+
scope(:not_jsonb_contains, lambda do |attributes|
|
36
|
+
QueryBuilder.new(self, attributes).contains_not_relation
|
37
|
+
end)
|
32
38
|
end
|
33
39
|
end
|
34
40
|
end
|
data/lib/attr_json/record.rb
CHANGED
@@ -47,7 +47,7 @@ module AttrJson
|
|
47
47
|
when false, nil, ActiveModel::Type::Boolean::FALSE_VALUES
|
48
48
|
false
|
49
49
|
else
|
50
|
-
if value.respond_to?(:to_i) && ( Numeric === value || value !~ /[^0-9]/ )
|
50
|
+
if value.respond_to?(:to_i) && ( Numeric === value || value.to_s !~ /[^0-9]/ )
|
51
51
|
!value.to_i.zero?
|
52
52
|
elsif value.respond_to?(:zero?)
|
53
53
|
!value.zero?
|
@@ -119,10 +119,11 @@ module AttrJson
|
|
119
119
|
# @option options [Boolean] :rails_attribute (false) Create an actual ActiveRecord
|
120
120
|
# `attribute` for name param. A Rails attribute isn't needed for our functionality,
|
121
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.
|
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)`
|
123
124
|
def attr_json(name, type, **options)
|
124
125
|
options = {
|
125
|
-
rails_attribute:
|
126
|
+
rails_attribute: self.attr_json_config.default_rails_attribute,
|
126
127
|
validate: true,
|
127
128
|
container_attribute: self.attr_json_config.default_container_attribute,
|
128
129
|
accepts_nested_attributes: self.attr_json_config.default_accepts_nested_attributes
|
@@ -160,7 +161,21 @@ module AttrJson
|
|
160
161
|
# We don't actually use this for anything, we provide our own covers. But registering
|
161
162
|
# it with usual system will let simple_form and maybe others find it.
|
162
163
|
if options[:rails_attribute]
|
163
|
-
|
164
|
+
attr_json_definition = attr_json_registry[name]
|
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
|
+
end
|
178
|
+
end
|
164
179
|
end
|
165
180
|
|
166
181
|
_attr_jsons_module.module_eval do
|
@@ -173,6 +188,7 @@ module AttrJson
|
|
173
188
|
# this simple way.
|
174
189
|
|
175
190
|
define_method("#{name}=") do |value|
|
191
|
+
super(value) if defined?(super)
|
176
192
|
attribute_def = self.class.attr_json_registry.fetch(name.to_sym)
|
177
193
|
public_send(attribute_def.container_attribute)[attribute_def.store_key] = attribute_def.cast(value)
|
178
194
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module AttrJson
|
2
|
+
|
3
|
+
# A little wrapper to provide an object that provides #dump and #load method for use
|
4
|
+
# as a coder second-argument for [ActiveRecord Serialization](https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html),
|
5
|
+
# that simply delegates to #serialize and #deserialize from a ActiveModel::Type object.
|
6
|
+
#
|
7
|
+
# Created to be used with an AttrJson::Model type (AttrJson::Type::Model), but hypothetically
|
8
|
+
# could be a shim from anything with serialize/deserialize to dump/load instead.
|
9
|
+
#
|
10
|
+
# class ValueModel
|
11
|
+
# include AttrJson::Model
|
12
|
+
# attr_json :some_string, :string
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# class SomeModel < ApplicationRecord
|
16
|
+
# serialize :some_json_column, ValueModel.to_serialize_coder
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# Note when used with an AttrJson::Model, it will dump/load from a HASH, not a
|
20
|
+
# string. It assumes it's writing to a Json(b) column that wants/provides hashes,
|
21
|
+
# not strings.
|
22
|
+
class SerializationCoderFromType
|
23
|
+
attr_reader :type
|
24
|
+
def initialize(type)
|
25
|
+
@type = type
|
26
|
+
end
|
27
|
+
|
28
|
+
# Dump and load methods to support ActiveRecord Serialization
|
29
|
+
# too.
|
30
|
+
def dump(value)
|
31
|
+
type.serialize(value)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Dump and load methods to support ActiveRecord Serialization
|
35
|
+
# too. https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html
|
36
|
+
def load(value)
|
37
|
+
type.deserialize(value)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/attr_json/type/model.rb
CHANGED
@@ -7,6 +7,7 @@ module AttrJson
|
|
7
7
|
# You create one with AttrJson::Model::Type.new(attr_json_model_class),
|
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
|
class Model < ::ActiveModel::Type::Value
|
11
12
|
class BadCast < ArgumentError ; end
|
12
13
|
|
@@ -31,15 +32,17 @@ module AttrJson
|
|
31
32
|
# to_hash is actually the 'implicit' conversion, it really is a hash
|
32
33
|
# even though it isn't is_a?(Hash), try to_hash first before to_h,
|
33
34
|
# the explicit conversion.
|
34
|
-
model.
|
35
|
+
model.new(v.to_hash)
|
35
36
|
elsif v.respond_to?(:to_h)
|
36
37
|
# TODO Maybe we ought not to do this on #to_h?
|
37
|
-
model.
|
38
|
+
model.new(v.to_h)
|
39
|
+
elsif model.attr_json_config.bad_cast == :as_nil
|
40
|
+
# This was originally default behavior, to be like existing ActiveRecord
|
41
|
+
# which kind of silently does this for non-castable basic values. That
|
42
|
+
# ended up being confusing in the basic case, so now we raise by default,
|
43
|
+
# but this is still configurable.
|
44
|
+
nil
|
38
45
|
else
|
39
|
-
# Bad input. Originally we were trying to return nil, to be like
|
40
|
-
# existing ActiveRecord which kind of silently does a basic value
|
41
|
-
# with null input. But that ended up making things confusing, let's
|
42
|
-
# just raise.
|
43
46
|
raise BadCast.new("Can not cast from #{v.inspect} to #{self.type}")
|
44
47
|
end
|
45
48
|
end
|
@@ -50,12 +53,36 @@ module AttrJson
|
|
50
53
|
elsif v.kind_of?(model)
|
51
54
|
v.serializable_hash
|
52
55
|
else
|
53
|
-
cast(v).serializable_hash
|
56
|
+
(cast_v = cast(v)) && cast_v.serializable_hash
|
54
57
|
end
|
55
58
|
end
|
56
59
|
|
57
60
|
def deserialize(v)
|
58
|
-
|
61
|
+
if v.nil?
|
62
|
+
# important to stay nil instead of empty object, because they
|
63
|
+
# are different things.
|
64
|
+
v
|
65
|
+
elsif v.kind_of? model
|
66
|
+
v
|
67
|
+
elsif v.respond_to?(:to_hash)
|
68
|
+
# to_hash is actually the 'implicit' conversion, it really is a hash
|
69
|
+
# even though it isn't is_a?(Hash), try to_hash first before to_h,
|
70
|
+
# the explicit conversion.
|
71
|
+
model.new_from_serializable(v.to_hash)
|
72
|
+
elsif v.respond_to?(:to_h)
|
73
|
+
# TODO Maybe we ought not to do this on #to_h? especially here in deserialize?
|
74
|
+
model.new_from_serializable(v.to_h)
|
75
|
+
elsif model.attr_json_config.bad_cast == :as_nil
|
76
|
+
# TODO should we have different config value for bad_deserialize vs bad_cast?
|
77
|
+
|
78
|
+
# This was originally default behavior, to be like existing ActiveRecord
|
79
|
+
# which kind of silently does this for non-castable basic values. That
|
80
|
+
# ended up being confusing in the basic case, so now we raise by default,
|
81
|
+
# but this is still configurable.
|
82
|
+
nil
|
83
|
+
else
|
84
|
+
raise BadCast.new("Can not cast from #{v.inspect} to #{self.type}")
|
85
|
+
end
|
59
86
|
end
|
60
87
|
|
61
88
|
# these guys are definitely mutable, so we need this.
|
@@ -51,6 +51,8 @@ module AttrJson
|
|
51
51
|
# MyRecord.jsonb_contains(author: { name: "foo", type: "Corporation"})
|
52
52
|
# MyRecord.jsonb_contains(author: Corporation.new(name: "foo"))
|
53
53
|
#
|
54
|
+
# Additionally, there is not_jsonb_contains, which creates the same query terms like jsonb_contains, but negated.
|
55
|
+
#
|
54
56
|
class PolymorphicModel < ActiveModel::Type::Value
|
55
57
|
class TypeError < ::TypeError ; end
|
56
58
|
|
@@ -112,6 +114,11 @@ module AttrJson
|
|
112
114
|
end
|
113
115
|
|
114
116
|
def serialize(v)
|
117
|
+
return nil if v.nil?
|
118
|
+
|
119
|
+
# if it's not already a model cast it to a model if possible (eg it's a hash)
|
120
|
+
v = cast(v)
|
121
|
+
|
115
122
|
model_name = v.class.name
|
116
123
|
type = type_for_model_name(model_name)
|
117
124
|
|
data/lib/attr_json/version.rb
CHANGED