diff --git a/README.md b/README.md
index 450b6562..c4e9c6e9 100644
--- a/README.md
+++ b/README.md
@@ -374,6 +374,13 @@ class PostSerializer < ActiveModel::Serializer
end
```
+## Serializing non-ActiveRecord objects
+
+All serializable resources must pass the ActiveModel::Serializer::Lint::Tests.
+
+See the ActiveModelSerializers::Model for a base class that implements the full
+API for a plain-old Ruby object (PORO).
+
## Getting Help
If you find a bug, please report an [Issue](https://github.com/rails-api/active_model_serializers/issues/new).
diff --git a/lib/active_model/serializer/lint.rb b/lib/active_model/serializer/lint.rb
index 29d564ed..eba88b1d 100644
--- a/lib/active_model/serializer/lint.rb
+++ b/lib/active_model/serializer/lint.rb
@@ -80,8 +80,8 @@ module ActiveModel::Serializer::Lint
# arguments (Rails 4.0) or a splat (Rails 4.1+).
# Fails otherwise.
#
- # cache_key returns a (self-expiring) unique key for the object,
- # which is used by the adapter.
+ # cache_key returns a (self-expiring) unique key for the object, and
+ # is part of the (self-expiring) cache_key, which is used by the adapter.
# It is not required unless caching is enabled.
def test_cache_key
assert_respond_to resource, :cache_key
@@ -92,6 +92,19 @@ module ActiveModel::Serializer::Lint
assert_includes [-1, 0], actual_arity, "expected #{actual_arity.inspect} to be 0 or -1"
end
+ # Passes if the object responds to updated_at and if it takes no
+ # arguments.
+ # Fails otherwise.
+ #
+ # updated_at returns a Time object or iso8601 string and
+ # is part of the (self-expiring) cache_key, which is used by the adapter.
+ # It is not required unless caching is enabled.
+ def test_updated_at
+ assert_respond_to resource, :updated_at
+ actual_arity = resource.method(:updated_at).arity
+ assert_equal actual_arity, 0, "expected #{actual_arity.inspect} to be 0"
+ end
+
# Passes if the object responds to id and if it takes no
# arguments.
# Fails otherwise.
diff --git a/lib/active_model_serializers.rb b/lib/active_model_serializers.rb
index 355393e5..922fd876 100644
--- a/lib/active_model_serializers.rb
+++ b/lib/active_model_serializers.rb
@@ -7,6 +7,9 @@ module ActiveModelSerializers
mattr_accessor :logger
self.logger = Rails.logger || Logger.new(IO::NULL)
+ extend ActiveSupport::Autoload
+ autoload :Model
+
module_function
# @note
diff --git a/lib/active_model_serializers/model.rb b/lib/active_model_serializers/model.rb
new file mode 100644
index 00000000..3043c389
--- /dev/null
+++ b/lib/active_model_serializers/model.rb
@@ -0,0 +1,39 @@
+# ActiveModelSerializers::Model is a convenient
+# serializable class to inherit from when making
+# serializable non-activerecord objects.
+module ActiveModelSerializers
+ class Model
+ include ActiveModel::Model
+ include ActiveModel::Serializers::JSON
+
+ attr_reader :attributes
+
+ def initialize(attributes = {})
+ @attributes = attributes
+ super
+ end
+
+ # Defaults to the downcased model name.
+ def id
+ attributes.fetch(:id) { self.class.name.downcase }
+ end
+
+ # Defaults to the downcased model name and updated_at
+ def cache_key
+ attributes.fetch(:cache_key) { "#{self.class.name.downcase}/#{id}-#{updated_at.strftime("%Y%m%d%H%M%S%9N")}" }
+ end
+
+ # Defaults to the time the serializer file was modified.
+ def updated_at
+ attributes.fetch(:updated_at) { File.mtime(__FILE__) }
+ end
+
+ def read_attribute_for_serialization(key)
+ if key == :id || key == 'id'
+ attributes.fetch(key) { id }
+ else
+ attributes[key]
+ end
+ end
+ end
+end
diff --git a/test/action_controller/adapter_selector_test.rb b/test/action_controller/adapter_selector_test.rb
index bb9a4c9e..55cff11e 100644
--- a/test/action_controller/adapter_selector_test.rb
+++ b/test/action_controller/adapter_selector_test.rb
@@ -46,7 +46,7 @@ module ActionController
def test_render_skipping_adapter
get :render_skipping_adapter
- assert_equal '{"attributes":{"name":"Name 1","description":"Description 1","comments":"Comments 1"}}', response.body
+ assert_equal '{"name":"Name 1","description":"Description 1","comments":"Comments 1"}', response.body
end
end
end
diff --git a/test/active_model_serializers/model_test.rb b/test/active_model_serializers/model_test.rb
new file mode 100644
index 00000000..141e86f9
--- /dev/null
+++ b/test/active_model_serializers/model_test.rb
@@ -0,0 +1,9 @@
+require 'test_helper'
+
+class ActiveModelSerializers::ModelTest < Minitest::Test
+ include ActiveModel::Serializer::Lint::Tests
+
+ def setup
+ @resource = ActiveModelSerializers::Model.new
+ end
+end
diff --git a/test/adapter/json/has_many_test.rb b/test/adapter/json/has_many_test.rb
index 34d69048..7e4037e5 100644
--- a/test/adapter/json/has_many_test.rb
+++ b/test/adapter/json/has_many_test.rb
@@ -36,7 +36,7 @@ module ActiveModel
assert_equal({
id: 42,
tags: [
- { 'attributes' => { 'id' => 1, 'name' => '#hash_tag' } }
+ { 'id' => 1, 'name' => '#hash_tag' }
]
}.to_json, adapter.serializable_hash[:post].to_json)
end
diff --git a/test/fixtures/poro.rb b/test/fixtures/poro.rb
index bfb9c84a..ca4a8b45 100644
--- a/test/fixtures/poro.rb
+++ b/test/fixtures/poro.rb
@@ -1,44 +1,16 @@
verbose = $VERBOSE
$VERBOSE = nil
-class Model
+class Model < ActiveModelSerializers::Model
FILE_DIGEST = Digest::MD5.hexdigest(File.open(__FILE__).read)
- def self.model_name
- @_model_name ||= ActiveModel::Name.new(self)
- end
-
- def initialize(hash = {})
- @attributes = hash
- end
-
- def cache_key
- "#{self.class.name.downcase}/#{self.id}-#{self.updated_at.strftime("%Y%m%d%H%M%S%9N")}"
- end
-
- def serializable_hash(options = nil)
- @attributes
- end
-
- def read_attribute_for_serialization(name)
- if name == :id || name == 'id'
- id
- else
- @attributes[name]
- end
- end
-
- def id
- @attributes[:id] || @attributes['id'] || object_id
- end
-
### Helper methods, not required to be serializable
- #
- # Convenience for adding @attributes readers and writers
+
+ # Convenience when not adding @attributes readers and writers
def method_missing(meth, *args)
if meth.to_s =~ /^(.*)=$/
- @attributes[$1.to_sym] = args[0]
- elsif @attributes.key?(meth)
- @attributes[meth]
+ attributes[$1.to_sym] = args[0]
+ elsif attributes.key?(meth)
+ attributes[meth]
else
super
end
@@ -47,10 +19,6 @@ class Model
def cache_key_with_digest
"#{cache_key}/#{FILE_DIGEST}"
end
-
- def updated_at
- @attributes[:updated_at] ||= DateTime.now.to_time
- end
end
class Profile < Model
diff --git a/test/lint_test.rb b/test/lint_test.rb
index 9257eec1..ca02124a 100644
--- a/test/lint_test.rb
+++ b/test/lint_test.rb
@@ -24,6 +24,9 @@ module ActiveModel
def id
end
+ def updated_at
+ end
+
def self.model_name
@_model_name ||= ActiveModel::Name.new(self)
end
diff --git a/test/serializers/associations_test.rb b/test/serializers/associations_test.rb
index 1748206a..10646907 100644
--- a/test/serializers/associations_test.rb
+++ b/test/serializers/associations_test.rb
@@ -54,7 +54,7 @@ module ActiveModel
assert_equal key, :tags
assert_equal serializer, nil
- assert_equal [{ attributes: { name: '#hashtagged' } }].to_json, options[:virtual_value].to_json
+ assert_equal [{ name: '#hashtagged' }].to_json, options[:virtual_value].to_json
end
end