-
Notifications
You must be signed in to change notification settings - Fork 361
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
9c8a2ee
commit 5f795d3
Showing
5 changed files
with
317 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
# frozen_string_literal: true | ||
|
||
module ActiveResource | ||
# Integrates with Active Record's | ||
# {serialize}[link:https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html#method-i-serialize] | ||
# method as the <tt>:coder</tt> option. | ||
# | ||
# Encodes Active Resource instances into a value to be stored in the | ||
# database. Decodes values read from the database into Active Resource | ||
# instances. | ||
# | ||
# class User < ActiveRecord::Base | ||
# serialize :person, coder: ActiveResource::Coder.new(Person) | ||
# end | ||
# | ||
# class Person < ActiveResource::Base | ||
# schema do | ||
# attribute :name, :string | ||
# end | ||
# end | ||
# | ||
# user = User.new | ||
# user.person = Person.new name: "Matz" | ||
# user.person.name # => "Matz" | ||
# | ||
# Values are loaded as persisted when decoded from data containing a | ||
# primary key value, and new records when missing a primary key value: | ||
# | ||
# user.person = Person.new | ||
# user.person.persisted? # => true | ||
# | ||
# user.person = Person.find(1) | ||
# user.person.persisted? # => true | ||
# | ||
# By default, <tt>#dump</tt> serializes the instance to a string value by | ||
# calling Base#encode: | ||
# | ||
# user.person_before_type_cast # => "{\"name\":\"Matz\"}" | ||
# | ||
# To customize serialization, pass the method name or a block as the second | ||
# argument: | ||
# | ||
# 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"} | ||
class Coder | ||
attr_accessor :resource_class, :encoder | ||
|
||
def initialize(resource_class, encoder_method = :encode, &block) | ||
@resource_class = resource_class | ||
@encoder = block || encoder_method | ||
end | ||
|
||
# Serializes a resource value to a value that will be stored in the database. | ||
# Returns nil when passed nil | ||
def dump(value) | ||
return if value.nil? | ||
raise ArgumentError, "expected value to be #{resource_class}, but was #{value.class}" unless value.is_a?(resource_class) | ||
|
||
value.yield_self(&encoder) | ||
end | ||
|
||
# Deserializes a value from the database to a resource instance. | ||
# Returns nil when passed nil | ||
def load(value) | ||
return if value.nil? | ||
|
||
if value.is_a?(String) | ||
load(resource_class.format.decode(value)) | ||
else | ||
persisted = value[resource_class.primary_key.to_s] | ||
resource_class.new(value, persisted) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
# frozen_string_literal: true | ||
|
||
module ActiveResource | ||
# Compatibilitiy with Active Record's | ||
# {serialize}[link:https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html#method-i-serialize] | ||
# method as the <tt>:coder</tt> 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. | ||
# | ||
# 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" | ||
# | ||
# Writing string values incorporates the Base.format: | ||
# | ||
# Person.format = :json | ||
# | ||
# user.person = Person.new name: "Matz" | ||
# user.person_before_type_cast # => "{\"name\":\"Matz\"}" | ||
# | ||
# 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: | ||
# | ||
# user.person = Person.new | ||
# user.person.persisted? # => false | ||
# | ||
# user.person = Person.find(1) | ||
# user.person.persisted? # => true | ||
# | ||
# === Writing to JSON and JSONB columns | ||
# | ||
# 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.name # => "Matz" | ||
# | ||
# user.person_before_type_cast # => {"name"=>"Matz"} | ||
module Serialization | ||
extend ActiveSupport::Concern | ||
|
||
included do | ||
class_attribute :coder, instance_accessor: false, instance_predicate: false | ||
end | ||
|
||
module ClassMethods | ||
delegate :dump, :load, to: :coder | ||
|
||
def inherited(subclass) # :nodoc: | ||
super | ||
subclass.coder = Coder.new(subclass) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
# frozen_string_literal: true | ||
|
||
require "abstract_unit" | ||
require "fixtures/person" | ||
require "active_support/core_ext/object/with" | ||
|
||
class SerializationTest < ActiveSupport::TestCase | ||
setup do | ||
@matz = { id: 1, name: "Matz" } | ||
end | ||
|
||
test ".load delegates to the .coder" do | ||
resource = Person.new(@matz) | ||
|
||
encoded = Person.load(resource.encode) | ||
|
||
assert_equal resource, encoded | ||
end | ||
|
||
test ".dump delegates to the default .coder" do | ||
resource = Person.new(@matz) | ||
|
||
encoded = Person.dump(resource) | ||
|
||
assert_equal resource.encode, encoded | ||
end | ||
|
||
test ".dump delegates to a configured .coder method name" do | ||
Person.with coder: ActiveResource::Coder.new(Person, :serializable_hash) do | ||
resource = Person.new(@matz) | ||
|
||
encoded = Person.dump(resource) | ||
|
||
assert_equal resource.serializable_hash, encoded | ||
end | ||
end | ||
|
||
test ".dump delegates to a configured .coder callable" do | ||
Person.with coder: ActiveResource::Coder.new(Person) { |value| value.serializable_hash } do | ||
resource = Person.new(@matz) | ||
|
||
encoded = Person.dump(resource) | ||
|
||
assert_equal resource.serializable_hash, encoded | ||
end | ||
end | ||
|
||
test "#load returns nil when the encoded value is nil" do | ||
assert_nil Person.coder.load(nil) | ||
end | ||
|
||
test "#load decodes a String into an instance" do | ||
resource = Person.new(@matz) | ||
|
||
decoded = Person.coder.load(resource.encode) | ||
|
||
assert_equal resource, decoded | ||
end | ||
|
||
test "#load decodes a Hash into an instance" do | ||
resource = Person.new(@matz) | ||
|
||
decoded = Person.coder.load(resource.serializable_hash) | ||
|
||
assert_equal resource, decoded | ||
end | ||
|
||
test "#load builds the instance as persisted when the default primary key is present" do | ||
resource = Person.new(@matz) | ||
|
||
decoded = Person.coder.load(resource.encode) | ||
|
||
assert_predicate decoded, :persisted? | ||
assert_not_predicate decoded, :new_record? | ||
end | ||
|
||
test "#load builds the instance as persisted when the configured primary key is present" do | ||
Person.primary_key = "pk" | ||
resource = Person.new(@matz.merge!(pk: @matz.delete(:id))) | ||
|
||
decoded = Person.coder.load(resource.encode) | ||
|
||
assert_predicate decoded, :persisted? | ||
assert_not_predicate decoded, :new_record? | ||
ensure | ||
Person.primary_key = "id" | ||
end | ||
|
||
test "#load builds the instance as a new record when the default primary key is absent" do | ||
resource = Person.new(@matz) | ||
resource.id = nil | ||
|
||
decoded = Person.coder.load(resource.encode) | ||
|
||
assert_not_predicate decoded, :persisted? | ||
assert_predicate decoded, :new_record? | ||
end | ||
|
||
test "#load builds the instance as a new record when the configured primary key is absent" do | ||
Person.primary_key = "pk" | ||
resource = Person.new(@matz) | ||
resource.id = nil | ||
|
||
decoded = Person.coder.load(resource.encode) | ||
|
||
assert_not_predicate decoded, :persisted? | ||
assert_predicate decoded, :new_record? | ||
|
||
Person.primary_key = "id" | ||
end | ||
|
||
test "#dump encodes resources" do | ||
resource = Person.new(@matz) | ||
|
||
encoded = Person.coder.dump(resource) | ||
|
||
assert_equal resource.encode, encoded | ||
end | ||
|
||
test "#dump raises an ArgumentError is passed anything but an ActiveResource::Base" do | ||
assert_raises ArgumentError, match: "expected value to be Person, but was Integer" do | ||
Person.coder.dump(1) | ||
end | ||
end | ||
|
||
test "#dump returns nil when the resource is nil" do | ||
assert_nil Person.coder.dump(nil) | ||
end | ||
|
||
test "#dump with an encoder method name returns nil when the resource is nil" do | ||
coder = ActiveResource::Coder.new(Person, :serializable_hash) | ||
|
||
assert_nil coder.dump(nil) | ||
end | ||
|
||
test "#dump with an encoder method name encodes resources" do | ||
coder = ActiveResource::Coder.new(Person, :serializable_hash) | ||
resource = Person.new(@matz) | ||
|
||
encoded = coder.dump(resource) | ||
|
||
assert_equal resource.serializable_hash, encoded | ||
end | ||
|
||
test "#dump with an encoder block encodes resources" do | ||
coder = ActiveResource::Coder.new(Person) { |value| value.serializable_hash } | ||
resource = Person.new(@matz) | ||
|
||
encoded = coder.dump(resource) | ||
|
||
assert_equal resource.serializable_hash, encoded | ||
end | ||
end |