Skip to content

Add PORO serializable base class: ActiveModelSerializers::Model #1272

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 20, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
17 changes: 15 additions & 2 deletions lib/active_model/serializer/lint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ def test_to_json
# arguments (Rails 4.0) or a splat (Rails 4.1+).
# Fails otherwise.
#
# <tt>cache_key</tt> returns a (self-expiring) unique key for the object,
# which is used by the adapter.
# <tt>cache_key</tt> 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
Expand All @@ -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 <tt>updated_at</tt> and if it takes no
# arguments.
# Fails otherwise.
#
# <tt>updated_at</tt> 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 <tt>id</tt> and if it takes no
# arguments.
# Fails otherwise.
Expand Down
3 changes: 3 additions & 0 deletions lib/active_model_serializers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 39 additions & 0 deletions lib/active_model_serializers/model.rb
Original file line number Diff line number Diff line change
@@ -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'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we make attributes a hash with indifferent access, we wouldn't need this || check.
I just think it's a little weird to only have indifferent access for the id.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to explain it but now I'm not sure

B mobile phone

On Oct 15, 2015, at 6:22 AM, L. Preston Sego III [email protected] wrote:

In lib/active_model_serializers/model.rb:

  • 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'
    
    if we make attributes a hash with indifferent access, we wouldn't need this || check.
    I just think it's a little weird to only have indifferent access for the id.


Reply to this email directly or view it on GitHub.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reasons I don't recall there might be an id method but not and :id attribute but it still hits method_missing...

attributes.fetch(key) { id }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the id method is missing, I probably shouldn't be trying to call it here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But, original code was

-    if name == :id || name == 'id'
-      id

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is just an example, is there anything wrong with just having attributes[key] ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it's used by the test suite like this... I'll double check tonight

B mobile phone

On Oct 15, 2015, at 3:46 PM, L. Preston Sego III [email protected] wrote:

In lib/active_model_serializers/model.rb:

  •  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 }
    
    Since this is just an example, is there anything wrong with just having attributes[key] ?


Reply to this email directly or view it on GitHub.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if id method is missing, shouldn't the it fail as a object complient resource?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NullVoxPopuli I ran some tests, and if only attributes[key] is present, it returns nil when the key is not present on the attributes hash

attributes => {"id"=>3, "title"=>"AMS"}
key => :body
attributes.fetch(key) { id } => 3
attributes[key] => nil

cc @bf4

else
attributes[key]
end
end
end
end
2 changes: 1 addition & 1 deletion test/action_controller/adapter_selector_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

going forward, can the expected value be assigned to an expected variable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, would be good

B mobile phone

On Oct 15, 2015, at 6:23 AM, L. Preston Sego III [email protected] wrote:

In 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
    
    going forward, can the expected value be assigned to an expected variable?


Reply to this email directly or view it on GitHub.

end
end
end
Expand Down
9 changes: 9 additions & 0 deletions test/active_model_serializers/model_test.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion test/adapter/json/has_many_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 6 additions & 38 deletions test/fixtures/poro.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions test/lint_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/serializers/associations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down