Skip to content

Commit 4798a43

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 bd154d2 commit 4798a43

File tree

8 files changed

+344
-22
lines changed

8 files changed

+344
-22
lines changed

lib/active_resource/base.rb

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,6 +1162,14 @@ def exists?(id, options = {})
11621162
false
11631163
end
11641164

1165+
def instantiate_collection(collection, original_params = {}, prefix_options = {}) # :nodoc:
1166+
collection_parser.new(collection).tap do |parser|
1167+
parser.resource_class = self
1168+
parser.original_params = original_params
1169+
parser.original_parsed = collection
1170+
end.collect! { |record| instantiate_record(record, prefix_options) }
1171+
end
1172+
11651173
private
11661174
def check_prefix_options(prefix_options)
11671175
p_options = HashWithIndifferentAccess.new(prefix_options)
@@ -1212,13 +1220,6 @@ def find_single(scope, options)
12121220
instantiate_record(format.decode(connection.get(path, headers).body), prefix_options)
12131221
end
12141222

1215-
def instantiate_collection(collection, original_params = {}, prefix_options = {})
1216-
collection_parser.new(collection).tap do |parser|
1217-
parser.resource_class = self
1218-
parser.original_params = original_params
1219-
end.collect! { |record| instantiate_record(record, prefix_options) }
1220-
end
1221-
12221223
def instantiate_record(record, prefix_options = {})
12231224
new(record, true).tap do |resource|
12241225
resource.prefix_options = prefix_options

lib/active_resource/coder.rb

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,23 +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

5378
# ==== Arguments
5479
# * <tt>resource_class</tt> Active Resource class that to be coded
5580
# * <tt>encoder_method</tt> the method to invoke on the instance to encode
5681
# it. Defaults to ActiveResource::Base#encode.
57-
def initialize(resource_class, encoder_method = :encode, &block)
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)
5888
@resource_class = resource_class
5989
@encoder = block || encoder_method
90+
@collection = collection
6091
end
6192

6293
# Serializes a resource value to a value that will be stored in the database.
6394
# Returns nil when passed nil
6495
def dump(value)
6596
return if value.nil?
66-
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)
67100

68101
value.yield_self(&encoder)
69102
end
@@ -73,10 +106,15 @@ def dump(value)
73106
def load(value)
74107
return if value.nil?
75108
value = resource_class.format.decode(value) if value.is_a?(String)
76-
raise ArgumentError.new("expected value to be Hash, but was #{value.class}") unless value.is_a?(Hash)
77-
value = Formats.remove_root(value) if value.keys.first.to_s == resource_class.element_name
78109

79-
resource_class.new(value, value[resource_class.primary_key])
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.instantiate_collection(value)
113+
else
114+
raise ArgumentError.new("expected value to be Hash, but was #{value.class}") unless value.is_a?(Hash)
115+
value = Formats.remove_root(value) if value.keys.first.to_s == resource_class.element_name
116+
resource_class.new(value, value[resource_class.primary_key])
117+
end
80118
end
81119
end
82120
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
@@ -66,11 +66,25 @@ module ActiveResource
6666
# user.person.name # => "Matz"
6767
#
6868
# user.person_before_type_cast # => {"name"=>"Matz"}
69+
#
70+
# === Collections
71+
#
72+
# To encode ActiveResource::Collection instances, pass the resource class
73+
# +collection_coder+ as the +:coder+ option:
74+
#
75+
# class Team < ActiveRecord::Base
76+
# serialize :people, coder: Person.collection_coder
77+
# end
78+
#
79+
# team = Team.new
80+
# team.people = Person.all
81+
# team.people.map(&:attributes) # => [{ "id" => 1, "name" => "Matz" }]
6982
module Serialization
7083
extend ActiveSupport::Concern
7184

7285
included do
7386
class_attribute :coder, instance_accessor: false, instance_predicate: false
87+
class_attribute :collection_coder, instance_accessor: false, instance_predicate: false
7488
end
7589

7690
module ClassMethods
@@ -79,6 +93,7 @@ module ClassMethods
7993
def inherited(subclass) # :nodoc:
8094
super
8195
subclass.coder = Coder.new(subclass)
96+
subclass.collection_coder = Coder.new(subclass, collection: true)
8297
end
8398
end
8499
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)