Skip to content

Commit 5f795d3

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 5f795d3

File tree

5 files changed

+317
-1
lines changed

5 files changed

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

0 commit comments

Comments
 (0)