Skip to content

Commit 64a7f52

Browse files
committed
Support .serialize …, coder: … for Collections
Follow-up to [#420][] Background --- The introduction of `ActiveResource::Base.coder` and the `ActiveResource::Coder` class added support for encoding and decoding instances of `Base`. However, collection-focused methods like `Base.where`, `Base.all`, and `Base.find(:all)` return `ActiveResource::Collection` instances. Problem --- While some collection instances are equivalent to `Array` instances, they are capable of being parsed into `Hash` values that include additional metadata (for example, pagination URLs, total counts, etc.). If applications were to dump results, there is a potential loss of that metadata. Proposal --- First, this commit modifies the `ActiveResource::Coder` class to accept a boolean `:collection` keyword to treat values as `ActiveResource::Collection` instances. Next, extend the `ActiveResource::Collection` class to retain the originally parsed values as a new `#original_parsed` attribute. It also defines the `#encode` method to rely on the resource class format for encoding. Additionally, extend the `ActiveResource::Serialization` module to also define a `.collection_coder` class attribute to serve as a convenience method for consumer to pass to `.serialize …, coder: …`: ```ruby class Person < ActiveResource::Base # … end class Team < ApplicationRecord serialize :people, coder: Person.collection_coder end ``` Like the instance-level coders, collection-level coders constructed with `collection: true` also accept an encoder proc to transform the value prior to dumping (for JSON/JSONB columns, for example): ```ruby class Person < ActiveResource::Base # … end class Team < ApplicationRecord # pass a to_proc-ready method name serialize :people, coder: ActiveResource::Coder.new(Person, :original_parsed, collection: true) # pass a block serialize :people, coder: ActiveResource::Coder.new(Person, collection: true) do |collection| collection.original_parsed end end ``` [#420]: #420
1 parent d488fe1 commit 64a7f52

File tree

9 files changed

+345
-16
lines changed

9 files changed

+345
-16
lines changed

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ branch = ENV.fetch("BRANCH", "main")
88
gem "activesupport", github: "rails/rails", branch: branch
99
gem "activemodel", github: "rails/rails", branch: branch
1010
gem "activejob", github: "rails/rails", branch: branch
11+
gem "activerecord", github: "rails/rails", branch: branch
12+
gem "sqlite3", branch == "7-0-stable" ? "~> 1.4" : nil
1113

1214
gem "rubocop"
1315
gem "rubocop-minitest"

lib/active_resource/base.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,6 +1163,7 @@ def instantiate_collection(collection, original_params = {}, prefix_options = {}
11631163
collection_parser.new(collection).tap do |parser|
11641164
parser.resource_class = self
11651165
parser.original_params = original_params
1166+
parser.original_parsed = collection
11661167
end.collect! { |record| instantiate_record(record, prefix_options) }
11671168
end
11681169

lib/active_resource/coder.rb

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ module ActiveResource
3737
#
3838
# user.person_before_type_cast # => "{\"name\":\"Matz\"}"
3939
#
40-
# To customize serialization, pass the method name or a block as the second
41-
# argument:
40+
# To customize serialization, pass the method name or a block that accepts the
41+
# instance as the second argument:
4242
#
4343
# person = Person.new name: "Matz"
4444
#
@@ -47,19 +47,56 @@ module ActiveResource
4747
#
4848
# coder = ActiveResource::Coder.new(Person) { |person| person.serializable_hash }
4949
# coder.dump(person) # => { "name" => "Matz" }
50+
#
51+
# === Collections
52+
#
53+
# To encode ActiveResource::Collection instances, construct an instance with +collection:
54+
# true+.
55+
#
56+
# class Team < ActiveRecord::Base
57+
# serialize :people, coder: ActiveResource::Coder.new(Person, collection: true)
58+
# end
59+
#
60+
# team = Team.new
61+
# team.people = Person.all
62+
# team.people.map(&:attributes) # => [{ "id" => 1, "name" => "Matz" }]
63+
#
64+
# By default, <tt>#dump</tt> serializes the instance to a string value by
65+
# calling Collection#encode:
66+
#
67+
# team.people_before_type_cast # => "[{\"id\":1,\"name\":\"Matz\"}]"
68+
#
69+
# To customize serialization, pass a block that accepts the collection as the second argument:
70+
#
71+
# people = Person.all
72+
#
73+
# coder = ActiveResource::Coder.new(Person) { |collection| collection.original_parsed }
74+
# coder.dump(people) # => [{ "id" => 1, "name" => "Matz" }]
5075
class Coder
51-
attr_accessor :resource_class, :encoder
76+
attr_accessor :resource_class, :encoder, :collection
5277

53-
def initialize(resource_class, encoder_method = :encode, &block)
78+
# ==== Arguments
79+
# * <tt>resource_class</tt> Active Resource class that to be coded
80+
# * <tt>encoder_method</tt> the method to invoke on the instance to encode
81+
# it. Defaults to ActiveResource::Base#encode.
82+
#
83+
# ==== Options
84+
#
85+
# * <tt>:collection</tt> - Whether or not the values reperesent an
86+
# ActiveResource::Collection Defaults to false.
87+
def initialize(resource_class, encoder_method = :encode, collection: false, &block)
5488
@resource_class = resource_class
5589
@encoder = block || encoder_method
90+
@collection = collection
5691
end
5792

5893
# Serializes a resource value to a value that will be stored in the database.
5994
# Returns nil when passed nil
6095
def dump(value)
6196
return if value.nil?
62-
raise ArgumentError.new("expected value to be #{resource_class}, but was #{value.class}") unless value.is_a?(resource_class)
97+
98+
expected_class = collection ? resource_class.collection_parser : resource_class
99+
raise ArgumentError.new("expected value to be #{expected_class}, but was #{value.class}") unless value.is_a?(expected_class)
63100

64101
value.yield_self(&encoder)
65102
end
@@ -69,8 +106,14 @@ def dump(value)
69106
def load(value)
70107
return if value.nil?
71108
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])
109+
110+
if collection
111+
raise ArgumentError.new("expected value to be Hash or Array, but was #{value.class}") unless value.is_a?(Hash) || value.is_a?(Array)
112+
resource_class.send(:instantiate_collection, value)
113+
else
114+
raise ArgumentError.new("expected value to be Hash, but was #{value.class}") unless value.is_a?(Hash)
115+
resource_class.new(value, value[resource_class.primary_key])
116+
end
74117
end
75118
end
76119
end

lib/active_resource/collection.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class Collection # :nodoc:
1010
delegate :to_yaml, :all?, *(Array.instance_methods(false) - SELF_DEFINE_METHODS), to: :to_a
1111

1212
# The array of actual elements returned by index actions
13-
attr_accessor :elements, :resource_class, :original_params
13+
attr_accessor :elements, :resource_class, :original_params, :original_parsed
1414

1515
# ActiveResource::Collection is a wrapper to handle parsing index responses that
1616
# do not directly map to Rails conventions.
@@ -90,5 +90,9 @@ def where(clauses = {})
9090
new_clauses = original_params.merge(clauses)
9191
resource_class.where(new_clauses)
9292
end
93+
94+
def encode
95+
resource_class.format.encode(original_parsed)
96+
end
9397
end
9498
end

lib/active_resource/serialization.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,25 @@ module ActiveResource
6262
# user.person.name # => "Matz"
6363
#
6464
# user.person_before_type_cast # => {"name"=>"Matz"}
65+
#
66+
# === Collections
67+
#
68+
# To encode ActiveResource::Collection instances, pass the resource class
69+
# +collection_coder+ as the +:coder+ option:
70+
#
71+
# class Team < ActiveRecord::Base
72+
# serialize :people, coder: Person.collection_coder
73+
# end
74+
#
75+
# team = Team.new
76+
# team.people = Person.all
77+
# team.people.map(&:attributes) # => [{ "id" => 1, "name" => "Matz" }]
6578
module Serialization
6679
extend ActiveSupport::Concern
6780

6881
included do
6982
class_attribute :coder, instance_accessor: false, instance_predicate: false
83+
class_attribute :collection_coder, instance_accessor: false, instance_predicate: false
7084
end
7185

7286
module ClassMethods
@@ -75,6 +89,7 @@ module ClassMethods
7589
def inherited(subclass) # :nodoc:
7690
super
7791
subclass.coder = Coder.new(subclass)
92+
subclass.collection_coder = Coder.new(subclass, collection: true)
7893
end
7994
end
8095
end

lib/active_resource/where_clause.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
module ActiveResource
44
class WhereClause < BasicObject # :nodoc:
5+
delegate :==, to: :resources
56
delegate_missing_to :resources
67

78
def initialize(resource_class, options = {})

0 commit comments

Comments
 (0)