Skip to content

Commit 124faaa

Browse files
committed
Add PORO serializable base class: ActiveModelSerializers::Model
1 parent 526b56e commit 124faaa

File tree

10 files changed

+85
-43
lines changed

10 files changed

+85
-43
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,13 @@ class PostSerializer < ActiveModel::Serializer
374374
end
375375
```
376376

377+
## Serializing non-ActiveRecord objects
378+
379+
All serializable resources must pass the ActiveModel::Serializer::Lint::Tests.
380+
381+
See the ActiveModelSerializers::Model for a base class that implements the full
382+
API for a plain-old Ruby object (PORO).
383+
377384
## Getting Help
378385

379386
If you find a bug, please report an [Issue](https://github.com/rails-api/active_model_serializers/issues/new).

lib/active_model/serializer/lint.rb

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ def test_to_json
8080
# arguments (Rails 4.0) or a splat (Rails 4.1+).
8181
# Fails otherwise.
8282
#
83-
# <tt>cache_key</tt> returns a (self-expiring) unique key for the object,
84-
# which is used by the adapter.
83+
# <tt>cache_key</tt> returns a (self-expiring) unique key for the object, and
84+
# is part of the (self-expiring) cache_key, which is used by the adapter.
8585
# It is not required unless caching is enabled.
8686
def test_cache_key
8787
assert_respond_to resource, :cache_key
@@ -92,6 +92,19 @@ def test_cache_key
9292
assert_includes [-1, 0], actual_arity, "expected #{actual_arity.inspect} to be 0 or -1"
9393
end
9494

95+
# Passes if the object responds to <tt>updated_at</tt> and if it takes no
96+
# arguments.
97+
# Fails otherwise.
98+
#
99+
# <tt>updated_at</tt> returns a Time object or iso8601 string and
100+
# is part of the (self-expiring) cache_key, which is used by the adapter.
101+
# It is not required unless caching is enabled.
102+
def test_updated_at
103+
assert_respond_to resource, :updated_at
104+
actual_arity = resource.method(:updated_at).arity
105+
assert_equal actual_arity, 0, "expected #{actual_arity.inspect} to be 0"
106+
end
107+
95108
# Passes if the object responds to <tt>id</tt> and if it takes no
96109
# arguments.
97110
# Fails otherwise.

lib/active_model_serializers.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ module ActiveModelSerializers
77
mattr_accessor :logger
88
self.logger = Rails.logger || Logger.new(IO::NULL)
99

10+
extend ActiveSupport::Autoload
11+
autoload :Model
12+
1013
module_function
1114

1215
# @note

lib/active_model_serializers/model.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# ActiveModelSerializers::Model is a convenient
2+
# serializable class to inherit from when making
3+
# serializable non-activerecord objects.
4+
module ActiveModelSerializers
5+
class Model
6+
include ActiveModel::Model
7+
include ActiveModel::Serializers::JSON
8+
9+
attr_reader :attributes
10+
11+
def initialize(attributes = {})
12+
@attributes = attributes
13+
super
14+
end
15+
16+
# Defaults to the downcased model name.
17+
def id
18+
attributes.fetch(:id) { self.class.name.downcase }
19+
end
20+
21+
# Defaults to the downcased model name and updated_at
22+
def cache_key
23+
attributes.fetch(:cache_key) { "#{self.class.name.downcase}/#{id}-#{updated_at.strftime("%Y%m%d%H%M%S%9N")}" }
24+
end
25+
26+
# Defaults to the time the serializer file was modified.
27+
def updated_at
28+
attributes.fetch(:updated_at) { File.mtime(__FILE__) }
29+
end
30+
31+
def read_attribute_for_serialization(key)
32+
if key == :id || key == 'id'
33+
attributes.fetch(key) { id }
34+
else
35+
attributes[key]
36+
end
37+
end
38+
end
39+
end

test/action_controller/adapter_selector_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def test_render_using_adapter_override
4646

4747
def test_render_skipping_adapter
4848
get :render_skipping_adapter
49-
assert_equal '{"attributes":{"name":"Name 1","description":"Description 1","comments":"Comments 1"}}', response.body
49+
assert_equal '{"name":"Name 1","description":"Description 1","comments":"Comments 1"}', response.body
5050
end
5151
end
5252
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
require 'test_helper'
2+
3+
class ActiveModelSerializers::ModelTest < Minitest::Test
4+
include ActiveModel::Serializer::Lint::Tests
5+
6+
def setup
7+
@resource = ActiveModelSerializers::Model.new
8+
end
9+
end

test/adapter/json/has_many_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_has_many_with_no_serializer
3636
assert_equal({
3737
id: 42,
3838
tags: [
39-
{ 'attributes' => { 'id' => 1, 'name' => '#hash_tag' } }
39+
{ 'id' => 1, 'name' => '#hash_tag' }
4040
]
4141
}.to_json, adapter.serializable_hash[:post].to_json)
4242
end

test/fixtures/poro.rb

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,16 @@
11
verbose = $VERBOSE
22
$VERBOSE = nil
3-
class Model
3+
class Model < ActiveModelSerializers::Model
44
FILE_DIGEST = Digest::MD5.hexdigest(File.open(__FILE__).read)
55

6-
def self.model_name
7-
@_model_name ||= ActiveModel::Name.new(self)
8-
end
9-
10-
def initialize(hash = {})
11-
@attributes = hash
12-
end
13-
14-
def cache_key
15-
"#{self.class.name.downcase}/#{self.id}-#{self.updated_at.strftime("%Y%m%d%H%M%S%9N")}"
16-
end
17-
18-
def serializable_hash(options = nil)
19-
@attributes
20-
end
21-
22-
def read_attribute_for_serialization(name)
23-
if name == :id || name == 'id'
24-
id
25-
else
26-
@attributes[name]
27-
end
28-
end
29-
30-
def id
31-
@attributes[:id] || @attributes['id'] || object_id
32-
end
33-
346
### Helper methods, not required to be serializable
35-
#
36-
# Convenience for adding @attributes readers and writers
7+
8+
# Convenience when not adding @attributes readers and writers
379
def method_missing(meth, *args)
3810
if meth.to_s =~ /^(.*)=$/
39-
@attributes[$1.to_sym] = args[0]
40-
elsif @attributes.key?(meth)
41-
@attributes[meth]
11+
attributes[$1.to_sym] = args[0]
12+
elsif attributes.key?(meth)
13+
attributes[meth]
4214
else
4315
super
4416
end
@@ -47,10 +19,6 @@ def method_missing(meth, *args)
4719
def cache_key_with_digest
4820
"#{cache_key}/#{FILE_DIGEST}"
4921
end
50-
51-
def updated_at
52-
@attributes[:updated_at] ||= DateTime.now.to_time
53-
end
5422
end
5523

5624
class Profile < Model

test/lint_test.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ def cache_key
2424
def id
2525
end
2626

27+
def updated_at
28+
end
29+
2730
def self.model_name
2831
@_model_name ||= ActiveModel::Name.new(self)
2932
end

test/serializers/associations_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def test_has_many_with_no_serializer
5454

5555
assert_equal key, :tags
5656
assert_equal serializer, nil
57-
assert_equal [{ attributes: { name: '#hashtagged' } }].to_json, options[:virtual_value].to_json
57+
assert_equal [{ name: '#hashtagged' }].to_json, options[:virtual_value].to_json
5858
end
5959
end
6060

0 commit comments

Comments
 (0)