From 124faaa8293e12780f2dbedfd595a4d1e0593662 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Fri, 9 Oct 2015 00:19:03 -0500 Subject: [PATCH] Add PORO serializable base class: ActiveModelSerializers::Model --- README.md | 7 +++ lib/active_model/serializer/lint.rb | 17 ++++++- lib/active_model_serializers.rb | 3 ++ lib/active_model_serializers/model.rb | 39 ++++++++++++++++ .../adapter_selector_test.rb | 2 +- test/active_model_serializers/model_test.rb | 9 ++++ test/adapter/json/has_many_test.rb | 2 +- test/fixtures/poro.rb | 44 +++---------------- test/lint_test.rb | 3 ++ test/serializers/associations_test.rb | 2 +- 10 files changed, 85 insertions(+), 43 deletions(-) create mode 100644 lib/active_model_serializers/model.rb create mode 100644 test/active_model_serializers/model_test.rb diff --git a/README.md b/README.md index 450b6562b..c4e9c6e96 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 29d564ed6..eba88b1d0 100644 --- a/lib/active_model/serializer/lint.rb +++ b/lib/active_model/serializer/lint.rb @@ -80,8 +80,8 @@ def test_to_json # 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 @@ def test_cache_key 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 355393e55..922fd876a 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 000000000..3043c389e --- /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 bb9a4c9eb..55cff11e4 100644 --- a/test/action_controller/adapter_selector_test.rb +++ b/test/action_controller/adapter_selector_test.rb @@ -46,7 +46,7 @@ def test_render_using_adapter_override 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 000000000..141e86f96 --- /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 34d69048f..7e4037e55 100644 --- a/test/adapter/json/has_many_test.rb +++ b/test/adapter/json/has_many_test.rb @@ -36,7 +36,7 @@ def test_has_many_with_no_serializer 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 bfb9c84a7..ca4a8b459 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 @@ def method_missing(meth, *args) 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 9257eec1e..ca02124a7 100644 --- a/test/lint_test.rb +++ b/test/lint_test.rb @@ -24,6 +24,9 @@ def cache_key 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 1748206a1..106469077 100644 --- a/test/serializers/associations_test.rb +++ b/test/serializers/associations_test.rb @@ -54,7 +54,7 @@ def test_has_many_with_no_serializer 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