Skip to content

Commit 9121114

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 9121114

File tree

5 files changed

+318
-1
lines changed

5 files changed

+318
-1
lines changed

lib/active_resource.rb

+2
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

+1-1
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

+80
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

+81
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

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

0 commit comments

Comments
 (0)