attr_json 0.7.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +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