Skip to content

Commit 86208db

Browse files
committed
Integrate with Active Record's .serialize
Define `ActiveResource::Base.dump` and `ActiveResource::Base.load` to support passing classes directly to [serialize][] as the `:coder` option: Writing to String columns --- Encodes Active Resource instances into a string to be stored in the database. Decodes strings read from the database into Active Resource instances. ```ruby class User < ActiveRecord::Base serialize :person, coder: Person end class Person < ActiveResource::Base schema do attribute :name, :string end end user = User.new user.person = Person.new name: "Matz" user.person_before_type_cast # => "{\"name\":\"Matz\"}" ``` Writing string values incorporates the Base.format: ```ruby Person.format = :xml user.person = Person.new name: "Matz" user.person_before_type_cast # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?><person><name>Matz</name></person>" ``` Instances are loaded as persisted when decoded from data containing a primary key value, and new records when missing a primary key value: ```ruby user.person = Person.new user.person.persisted? # => false user.person = Person.find(1) user.person.persisted? # => true ``` Writing to JSON and JSONB columns --- ```ruby class User < ActiveRecord::Base serialize :person, coder: ActiveResource::Coder.new(Person, :serializable_hash) end class Person < ActiveResource::Base schema do attribute :name, :string end end user = User.new user.person = Person.new name: "Matz" user.person_before_type_cast # => {"name"=>"Matz"} user.person.name # => "Matz" ``` The `ActiveResource::Coder` class === By default, `#dump` serializes the instance to a string value by calling `ActiveResource::Base#encode`: ```ruby user.person_before_type_cast # => "{\"name\":\"Matz\"}" ``` To customize serialization, pass the method name or a block as the second argument: ```ruby person = Person.new name: "Matz" coder = ActiveResource::Coder.new(Person, :serializable_hash) coder.dump(person) # => {"name"=>"Matz"} coder = ActiveResource::Coder.new(Person) { |person| person.serializable_hash } coder.dump(person) # => {"name"=>"Matz"} ``` [serialize]: https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html#method-i-serialize
1 parent 6dcd314 commit 86208db

File tree

5 files changed

+358
-1
lines changed

5 files changed

+358
-1
lines changed

lib/active_resource.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ module ActiveResource
3737

3838
autoload :Base
3939
autoload :Callbacks
40+
autoload :Coder
4041
autoload :Connection
4142
autoload :CustomMethods
4243
autoload :Formats
4344
autoload :HttpMock
4445
autoload :Rescuable
4546
autoload :Schema
47+
autoload :Serialization
4648
autoload :Singleton
4749
autoload :InheritingHash
4850
autoload :Validations

lib/active_resource/base.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1751,7 +1751,7 @@ class Base
17511751
extend ActiveModel::Naming
17521752
extend ActiveResource::Associations
17531753

1754-
include Callbacks, CustomMethods, Validations
1754+
include Callbacks, CustomMethods, Validations, Serialization
17551755
include ActiveModel::Conversion
17561756
include ActiveModel::ForbiddenAttributesProtection
17571757
include ActiveModel::Serializers::JSON

lib/active_resource/coder.rb

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveResource
4+
# Integrates with Active Record's
5+
# {serialize}[link:https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html#method-i-serialize]
6+
# method as the <tt>:coder</tt> option.
7+
#
8+
# Encodes Active Resource instances into a value to be stored in the
9+
# database. Decodes values read from the database into Active Resource
10+
# instances.
11+
#
12+
# class User < ActiveRecord::Base
13+
# serialize :person, coder: ActiveResource::Coder.new(Person)
14+
# end
15+
#
16+
# class Person < ActiveResource::Base
17+
# schema do
18+
# attribute :name, :string
19+
# end
20+
# end
21+
#
22+
# user = User.new
23+
# user.person = Person.new name: "Matz"
24+
# user.person.name # => "Matz"
25+
#
26+
# Values are loaded as persisted when decoded from data containing a
27+
# primary key value, and new records when missing a primary key value:
28+
#
29+
# user.person = Person.new
30+
# user.person.persisted? # => true
31+
#
32+
# user.person = Person.find(1)
33+
# user.person.persisted? # => true
34+
#
35+
# By default, <tt>#dump</tt> serializes the instance to a string value by
36+
# calling Base#encode:
37+
#
38+
# user.person_before_type_cast # => "{\"name\":\"Matz\"}"
39+
#
40+
# To customize serialization, pass the method name or a block as the second
41+
# argument:
42+
#
43+
# person = Person.new name: "Matz"
44+
#
45+
# coder = ActiveResource::Coder.new(Person, :serializable_hash)
46+
# coder.dump(person) # => { "name" => "Matz" }
47+
#
48+
# coder = ActiveResource::Coder.new(Person) { |person| person.serializable_hash }
49+
# coder.dump(person) # => { "name" => "Matz" }
50+
class Coder
51+
attr_accessor :resource_class, :encoder
52+
53+
def initialize(resource_class, encoder_method = :encode, &block)
54+
@resource_class = resource_class
55+
@encoder = block || encoder_method
56+
end
57+
58+
# Serializes a resource value to a value that will be stored in the database.
59+
# Returns nil when passed nil
60+
def dump(value)
61+
return if value.nil?
62+
raise ArgumentError.new("expected value to be #{resource_class}, but was #{value.class}") unless value.is_a?(resource_class)
63+
64+
value.yield_self(&encoder)
65+
end
66+
67+
# Deserializes a value from the database to a resource instance.
68+
# Returns nil when passed nil
69+
def load(value)
70+
return if value.nil?
71+
value = resource_class.format.decode(value) if value.is_a?(String)
72+
raise ArgumentError.new("expected value to be Hash, but was #{value.class}") unless value.is_a?(Hash)
73+
resource_class.new(value, value[resource_class.primary_key])
74+
end
75+
end
76+
end
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveResource
4+
# Compatibilitiy with Active Record's
5+
# {serialize}[link:https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html#method-i-serialize]
6+
# method as the <tt>:coder</tt> option.
7+
#
8+
# === Writing to String columns
9+
#
10+
# Encodes Active Resource instances into a string to be stored in the
11+
# database. Decodes strings read from the database into Active Resource
12+
# instances.
13+
#
14+
# class User < ActiveRecord::Base
15+
# serialize :person, coder: Person
16+
# end
17+
#
18+
# class Person < ActiveResource::Base
19+
# schema do
20+
# attribute :name, :string
21+
# end
22+
# end
23+
#
24+
# user = User.new
25+
# user.person = Person.new name: "Matz"
26+
#
27+
# Writing string values incorporates the Base.format:
28+
#
29+
# Person.format = :json
30+
#
31+
# user.person = Person.new name: "Matz"
32+
# user.person_before_type_cast # => "{\"name\":\"Matz\"}"
33+
#
34+
# Person.format = :xml
35+
#
36+
# user.person = Person.new name: "Matz"
37+
# user.person_before_type_cast # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?><person><name>Matz</name></person>"
38+
#
39+
# Instances are loaded as persisted when decoded from data containing a
40+
# primary key value, and new records when missing a primary key value:
41+
#
42+
# user.person = Person.new
43+
# user.person.persisted? # => false
44+
#
45+
# user.person = Person.find(1)
46+
# user.person.persisted? # => true
47+
#
48+
# === Writing to JSON and JSONB columns
49+
#
50+
# class User < ActiveRecord::Base
51+
# serialize :person, coder: ActiveResource::Coder.new(Person, :serializable_hash)
52+
# end
53+
#
54+
# class Person < ActiveResource::Base
55+
# schema do
56+
# attribute :name, :string
57+
# end
58+
# end
59+
#
60+
# user = User.new
61+
# user.person = Person.new name: "Matz"
62+
# user.person.name # => "Matz"
63+
#
64+
# user.person_before_type_cast # => {"name"=>"Matz"}
65+
module Serialization
66+
extend ActiveSupport::Concern
67+
68+
included do
69+
class_attribute :coder, instance_accessor: false, instance_predicate: false
70+
end
71+
72+
module ClassMethods
73+
delegate :dump, :load, to: :coder
74+
75+
def inherited(subclass) # :nodoc:
76+
super
77+
subclass.coder = Coder.new(subclass)
78+
end
79+
end
80+
end
81+
end
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# frozen_string_literal: true
2+
3+
require "abstract_unit"
4+
require "fixtures/person"
5+
require "active_support/core_ext/object/with"
6+
7+
class SerializationTest < ActiveSupport::TestCase
8+
test ".load delegates to the .coder" do
9+
resource = Person.new(id: 1, name: "Matz")
10+
11+
encoded = Person.load(resource.encode)
12+
13+
assert_equal resource.id, encoded.id
14+
assert_equal resource.name, encoded.name
15+
assert_equal resource.attributes, encoded.attributes
16+
end
17+
18+
test ".load decodes JSON" do
19+
Person.with format: :json do
20+
resource = Person.new(id: 1, name: "Matz")
21+
json = resource.to_json
22+
23+
assert_equal resource, Person.load(json)
24+
end
25+
end
26+
27+
test ".load decodes XML" do
28+
Person.with format: :xml do
29+
resource = Person.new(id: 1, name: "Matz")
30+
xml = resource.to_xml
31+
32+
assert_equal resource, Person.load(xml)
33+
end
34+
end
35+
36+
test ".dump delegates to the default .coder" do
37+
resource = Person.new(id: 1, name: "Matz")
38+
39+
encoded = Person.dump(resource)
40+
41+
assert_equal resource.encode, encoded
42+
assert_equal({ person: { id: 1, name: "Matz" } }.to_json, encoded)
43+
end
44+
45+
test ".dump delegates to a configured .coder method name" do
46+
Person.with coder: ActiveResource::Coder.new(Person, :serializable_hash) do
47+
resource = Person.new(id: 1, name: "Matz")
48+
49+
encoded = Person.dump(resource)
50+
51+
assert_equal resource.serializable_hash, encoded
52+
end
53+
end
54+
55+
test ".dump delegates to a configured .coder callable" do
56+
Person.with coder: ActiveResource::Coder.new(Person) { |value| value.serializable_hash } do
57+
resource = Person.new(id: 1, name: "Matz")
58+
59+
encoded = Person.dump(resource)
60+
61+
assert_equal resource.serializable_hash, encoded
62+
end
63+
end
64+
65+
test ".dump encodes JSON" do
66+
Person.with format: :json do
67+
resource = Person.new(id: 1, name: "Matz")
68+
69+
assert_equal resource.to_json, Person.dump(resource)
70+
end
71+
end
72+
73+
test ".dump encodes XML" do
74+
Person.with format: :xml do
75+
resource = Person.new(id: 1, name: "Matz")
76+
77+
assert_equal resource.to_xml, Person.dump(resource)
78+
end
79+
end
80+
81+
test "#load returns nil when the encoded value is nil" do
82+
assert_nil Person.coder.load(nil)
83+
end
84+
85+
test "#load decodes a String into an instance" do
86+
resource = Person.new(id: 1, name: "Matz")
87+
88+
decoded = Person.coder.load(resource.encode)
89+
90+
assert_equal resource, decoded
91+
end
92+
93+
test "#load decodes a Hash into an instance" do
94+
resource = Person.new(id: 1, name: "Matz")
95+
96+
decoded = Person.coder.load(resource.serializable_hash)
97+
98+
assert_equal resource.id, decoded.id
99+
assert_equal resource.name, decoded.name
100+
assert_equal resource.attributes, decoded.attributes
101+
end
102+
103+
test "#load builds the instance as persisted when the default primary key is present" do
104+
resource = Person.new(id: 1, name: "Matz")
105+
106+
decoded = Person.coder.load(resource.encode)
107+
108+
assert_predicate decoded, :persisted?
109+
assert_not_predicate decoded, :new_record?
110+
end
111+
112+
test "#load builds the instance as persisted when the configured primary key is present" do
113+
Person.with primary_key: "pk" do
114+
resource = Person.new(pk: 1, name: "Matz")
115+
116+
decoded = Person.coder.load(resource.encode)
117+
118+
assert_equal 1, decoded.id
119+
assert_predicate decoded, :persisted?
120+
assert_not_predicate decoded, :new_record?
121+
end
122+
end
123+
124+
test "#load builds the instance as a new record when the default primary key is absent" do
125+
resource = Person.new(name: "Matz")
126+
127+
decoded = Person.coder.load(resource.encode)
128+
129+
assert_nil decoded.id
130+
assert_not_predicate decoded, :persisted?
131+
assert_predicate decoded, :new_record?
132+
end
133+
134+
test "#load builds the instance as a new record when the configured primary key is absent" do
135+
Person.with primary_key: "pk" do
136+
resource = Person.new(name: "Matz")
137+
138+
decoded = Person.coder.load(resource.encode)
139+
140+
assert_nil decoded.id
141+
assert_not_predicate decoded, :persisted?
142+
assert_predicate decoded, :new_record?
143+
end
144+
end
145+
146+
test "#load raises an ArgumentError when passed anything but a String or Hash" do
147+
resource = Person.new(name: "Matz")
148+
string_value = resource.encode
149+
hash_value = resource.attributes
150+
151+
assert_equal resource, Person.coder.load(string_value)
152+
assert_equal resource, Person.coder.load(hash_value)
153+
assert_raises(ArgumentError, match: "expected value to be Hash, but was Integer") { Person.coder.load(1) }
154+
end
155+
156+
test "#dump encodes resources" do
157+
resource = Person.new(id: 1, name: "Matz")
158+
159+
encoded = Person.coder.dump(resource)
160+
161+
assert_equal resource.encode, encoded
162+
assert_equal({ person: { id: 1, name: "Matz" } }.to_json, encoded)
163+
end
164+
165+
test "#dump raises an ArgumentError is passed anything but an ActiveResource::Base" do
166+
assert_raises ArgumentError, match: "expected value to be Person, but was Integer" do
167+
Person.coder.dump(1)
168+
end
169+
end
170+
171+
test "#dump returns nil when the resource is nil" do
172+
assert_nil Person.coder.dump(nil)
173+
end
174+
175+
test "#dump with an encoder method name returns nil when the resource is nil" do
176+
coder = ActiveResource::Coder.new(Person, :serializable_hash)
177+
178+
assert_nil coder.dump(nil)
179+
end
180+
181+
test "#dump with an encoder method name encodes resources" do
182+
coder = ActiveResource::Coder.new(Person, :serializable_hash)
183+
resource = Person.new(id: 1, name: "Matz")
184+
185+
encoded = coder.dump(resource)
186+
187+
assert_equal resource.serializable_hash, encoded
188+
end
189+
190+
test "#dump with an encoder block encodes resources" do
191+
coder = ActiveResource::Coder.new(Person) { |value| value.serializable_hash }
192+
resource = Person.new(id: 1, name: "Matz")
193+
194+
encoded = coder.dump(resource)
195+
196+
assert_equal resource.serializable_hash, encoded
197+
end
198+
end

0 commit comments

Comments
 (0)