Skip to content

Commit 072972f

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 9c8a2ee commit 072972f

File tree

5 files changed

+321
-1
lines changed

5 files changed

+321
-1
lines changed

lib/active_resource.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ 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 :Schema
46+
autoload :Serialization
4547
autoload :Singleton
4648
autoload :InheritingHash
4749
autoload :Validations

lib/active_resource/base.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1721,7 +1721,7 @@ class Base
17211721
extend ActiveModel::Naming
17221722
extend ActiveResource::Associations
17231723

1724-
include Callbacks, CustomMethods, Validations
1724+
include Callbacks, CustomMethods, Validations, Serialization
17251725
include ActiveModel::Conversion
17261726
include ActiveModel::Serializers::JSON
17271727
include ActiveModel::Serializers::Xml

lib/active_resource/coder.rb

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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, "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+
72+
if value.is_a?(String)
73+
load(resource_class.format.decode(value))
74+
else
75+
persisted = value[resource_class.primary_key.to_s]
76+
resource_class.new(value, persisted)
77+
end
78+
end
79+
end
80+
end

lib/active_resource/serialization.rb

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

test/cases/base/serialization_test.rb

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# frozen_string_literal: true
2+
3+
require "abstract_unit"
4+
require "fixtures/person"
5+
6+
class SerializationTest < ActiveSupport::TestCase
7+
test ".load delegates to the .coder" do
8+
resource = Person.new(id: 1, name: "Matz")
9+
10+
encoded = Person.load(resource.encode)
11+
12+
assert_equal resource.id, encoded.id
13+
assert_equal resource.name, encoded.name
14+
assert_equal resource.attributes, encoded.attributes
15+
end
16+
17+
test ".dump delegates to the default .coder" do
18+
resource = Person.new(id: 1, name: "Matz")
19+
20+
encoded = Person.dump(resource)
21+
22+
assert_equal resource.encode, encoded
23+
assert_equal({ person: { id: 1, name: "Matz" } }.to_json, encoded)
24+
end
25+
26+
test ".dump delegates to a configured .coder method name" do
27+
Person.coder = ActiveResource::Coder.new(Person, :serializable_hash)
28+
resource = Person.new(id: 1, name: "Matz")
29+
30+
encoded = Person.dump(resource)
31+
32+
assert_equal resource.serializable_hash, encoded
33+
ensure
34+
Person.coder = ActiveResource::Coder.new(Person)
35+
end
36+
37+
test ".dump delegates to a configured .coder callable" do
38+
Person.coder = ActiveResource::Coder.new(Person) { |value| value.serializable_hash }
39+
resource = Person.new(id: 1, name: "Matz")
40+
41+
encoded = Person.dump(resource)
42+
43+
assert_equal resource.serializable_hash, encoded
44+
ensure
45+
Person.coder = ActiveResource::Coder.new(Person)
46+
end
47+
48+
test "#load returns nil when the encoded value is nil" do
49+
assert_nil Person.coder.load(nil)
50+
end
51+
52+
test "#load decodes a String into an instance" do
53+
resource = Person.new(id: 1, name: "Matz")
54+
55+
decoded = Person.coder.load(resource.encode)
56+
57+
assert_equal resource, decoded
58+
end
59+
60+
test "#load decodes a Hash into an instance" do
61+
resource = Person.new(id: 1, name: "Matz")
62+
63+
decoded = Person.coder.load(resource.serializable_hash)
64+
65+
assert_equal resource.id, decoded.id
66+
assert_equal resource.name, decoded.name
67+
assert_equal resource.attributes, decoded.attributes
68+
end
69+
70+
test "#load builds the instance as persisted when the default primary key is present" do
71+
resource = Person.new(id: 1, name: "Matz")
72+
73+
decoded = Person.coder.load(resource.encode)
74+
75+
assert_predicate decoded, :persisted?
76+
assert_not_predicate decoded, :new_record?
77+
end
78+
79+
test "#load builds the instance as persisted when the configured primary key is present" do
80+
Person.primary_key = "pk"
81+
resource = Person.new(pk: 1, name: "Matz")
82+
83+
decoded = Person.coder.load(resource.encode)
84+
85+
assert_equal 1, decoded.id
86+
assert_predicate decoded, :persisted?
87+
assert_not_predicate decoded, :new_record?
88+
ensure
89+
Person.primary_key = "id"
90+
end
91+
92+
test "#load builds the instance as a new record when the default primary key is absent" do
93+
resource = Person.new(name: "Matz")
94+
95+
decoded = Person.coder.load(resource.encode)
96+
97+
assert_nil decoded.id
98+
assert_not_predicate decoded, :persisted?
99+
assert_predicate decoded, :new_record?
100+
end
101+
102+
test "#load builds the instance as a new record when the configured primary key is absent" do
103+
Person.primary_key = "pk"
104+
resource = Person.new(name: "Matz")
105+
106+
decoded = Person.coder.load(resource.encode)
107+
108+
assert_nil decoded.id
109+
assert_not_predicate decoded, :persisted?
110+
assert_predicate decoded, :new_record?
111+
112+
Person.primary_key = "id"
113+
end
114+
115+
test "#dump encodes resources" do
116+
resource = Person.new(id: 1, name: "Matz")
117+
118+
encoded = Person.coder.dump(resource)
119+
120+
assert_equal resource.encode, encoded
121+
assert_equal({ person: { id: 1, name: "Matz" } }.to_json, encoded)
122+
end
123+
124+
test "#dump raises an ArgumentError is passed anything but an ActiveResource::Base" do
125+
assert_raises ArgumentError, match: "expected value to be Person, but was Integer" do
126+
Person.coder.dump(1)
127+
end
128+
end
129+
130+
test "#dump returns nil when the resource is nil" do
131+
assert_nil Person.coder.dump(nil)
132+
end
133+
134+
test "#dump with an encoder method name returns nil when the resource is nil" do
135+
coder = ActiveResource::Coder.new(Person, :serializable_hash)
136+
137+
assert_nil coder.dump(nil)
138+
end
139+
140+
test "#dump with an encoder method name encodes resources" do
141+
coder = ActiveResource::Coder.new(Person, :serializable_hash)
142+
resource = Person.new(id: 1, name: "Matz")
143+
144+
encoded = coder.dump(resource)
145+
146+
assert_equal resource.serializable_hash, encoded
147+
end
148+
149+
test "#dump with an encoder block encodes resources" do
150+
coder = ActiveResource::Coder.new(Person) { |value| value.serializable_hash }
151+
resource = Person.new(id: 1, name: "Matz")
152+
153+
encoded = coder.dump(resource)
154+
155+
assert_equal resource.serializable_hash, encoded
156+
end
157+
end

0 commit comments

Comments
 (0)