diff options
18 files changed, 1 insertions, 1268 deletions
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 3829e60bb0..f4eaa2fd1b 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -31,7 +31,6 @@ module ActionController autoload :RequestForgeryProtection autoload :Rescue autoload :Responder - autoload :Serialization autoload :SessionManagement autoload :Streaming autoload :Testing diff --git a/actionpack/lib/action_controller/metal/serialization.rb b/actionpack/lib/action_controller/metal/serialization.rb deleted file mode 100644 index 628d5996d7..0000000000 --- a/actionpack/lib/action_controller/metal/serialization.rb +++ /dev/null @@ -1,51 +0,0 @@ -module ActionController - # Action Controller Serialization - # - # Overrides render :json to check if the given object implements +active_model_serializer+ - # as a method. If so, use the returned serializer instead of calling +to_json+ in the object. - # - # This module also provides a serialization_scope method that allows you to configure the - # +serialization_scope+ of the serializer. Most apps will likely set the +serialization_scope+ - # to the current user: - # - # class ApplicationController < ActionController::Base - # serialization_scope :current_user - # end - # - # If you need more complex scope rules, you can simply override the serialization_scope: - # - # class ApplicationController < ActionController::Base - # private - # - # def serialization_scope - # current_user - # end - # end - # - module Serialization - extend ActiveSupport::Concern - - include ActionController::Renderers - - included do - class_attribute :_serialization_scope - end - - def serialization_scope - send(_serialization_scope) - end - - def _render_option_json(json, options) - if json.respond_to?(:active_model_serializer) && (serializer = json.active_model_serializer) - json = serializer.new(json, serialization_scope) - end - super - end - - module ClassMethods - def serialization_scope(scope) - self._serialization_scope = scope - end - end - end -end diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb index dc09812ba3..fc604a2db3 100644 --- a/actionpack/test/controller/render_json_test.rb +++ b/actionpack/test/controller/render_json_test.rb @@ -15,36 +15,9 @@ class RenderJsonTest < ActionController::TestCase end end - class JsonSerializer - def initialize(object, scope) - @object, @scope = object, scope - end - - def as_json(*) - { :object => @object.as_json, :scope => @scope.as_json } - end - end - - class JsonSerializable - def initialize(skip=false) - @skip = skip - end - - def active_model_serializer - JsonSerializer unless @skip - end - - def as_json(*) - { :serializable_object => true } - end - end - class TestController < ActionController::Base protect_from_forgery - serialization_scope :current_user - attr_reader :current_user - def self.controller_path 'test' end @@ -88,16 +61,6 @@ class RenderJsonTest < ActionController::TestCase def render_json_without_options render :json => JsonRenderable.new end - - def render_json_with_serializer - @current_user = Struct.new(:as_json).new(:current_user => true) - render :json => JsonSerializable.new - end - - def render_json_with_serializer_api_but_without_serializer - @current_user = Struct.new(:as_json).new(:current_user => true) - render :json => JsonSerializable.new(true) - end end tests TestController @@ -169,15 +132,4 @@ class RenderJsonTest < ActionController::TestCase get :render_json_without_options assert_equal '{"a":"b"}', @response.body end - - def test_render_json_with_serializer - get :render_json_with_serializer - assert_match '"scope":{"current_user":true}', @response.body - assert_match '"object":{"serializable_object":true}', @response.body - end - - def test_render_json_with_serializer_api_but_without_serializer - get :render_json_with_serializer_api_but_without_serializer - assert_match '{"serializable_object":true}', @response.body - end end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 53934f08e7..caea0b86bd 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,7 +1,5 @@ ## Rails 3.2.0 (unreleased) ## -* Add ActiveModel::Serializer that encapsulates an ActiveModel object serialization *José Valim* - * Renamed (with a deprecation the following constants): ActiveModel::Serialization => ActiveModel::Serializable diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 6c4fb44b0f..7ea04344f0 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -29,7 +29,6 @@ require 'active_model/version' module ActiveModel extend ActiveSupport::Autoload - autoload :ArraySerializer, 'active_model/serializer' autoload :AttributeMethods autoload :BlockValidator, 'active_model/validator' autoload :Callbacks @@ -46,7 +45,6 @@ module ActiveModel autoload :SecurePassword autoload :Serializable autoload :Serialization - autoload :Serializer autoload :TestCase autoload :Translation autoload :Validations diff --git a/activemodel/lib/active_model/serializable.rb b/activemodel/lib/active_model/serializable.rb index 70e27c5683..86770a25e4 100644 --- a/activemodel/lib/active_model/serializable.rb +++ b/activemodel/lib/active_model/serializable.rb @@ -73,13 +73,6 @@ module ActiveModel autoload :JSON, "active_model/serializable/json" autoload :XML, "active_model/serializable/xml" - module ClassMethods #:nodoc: - def active_model_serializer - return @active_model_serializer if defined?(@active_model_serializer) - @active_model_serializer = "#{self.name}Serializer".safe_constantize - end - end - def serializable_hash(options = nil) options ||= {} @@ -107,11 +100,6 @@ module ActiveModel hash end - # Returns a model serializer for this object considering its namespace. - def active_model_serializer - self.class.active_model_serializer - end - private # Hook method defining how an attribute value should be retrieved for diff --git a/activemodel/test/cases/serializer_test.rb b/activemodel/test/cases/serializer_test.rb deleted file mode 100644 index 2b2e21540f..0000000000 --- a/activemodel/test/cases/serializer_test.rb +++ /dev/null @@ -1,432 +0,0 @@ -require "cases/helper" - -class SerializerTest < ActiveModel::TestCase - class Model - def initialize(hash={}) - @attributes = hash - end - - def read_attribute_for_serialization(name) - @attributes[name] - end - - def as_json(*) - { :model => "Model" } - end - end - - class User - include ActiveModel::Serializable - - attr_accessor :superuser - - def initialize(hash={}) - @attributes = hash.merge(:first_name => "Jose", :last_name => "Valim", :password => "oh noes yugive my password") - end - - def read_attribute_for_serialization(name) - @attributes[name] - end - - def super_user? - @superuser - end - end - - class Post < Model - attr_accessor :comments - def active_model_serializer; PostSerializer; end - end - - class Comment < Model - def active_model_serializer; CommentSerializer; end - end - - class UserSerializer < ActiveModel::Serializer - attributes :first_name, :last_name - - def serializable_hash - attributes.merge(:ok => true).merge(scope) - end - end - - class DefaultUserSerializer < ActiveModel::Serializer - attributes :first_name, :last_name - end - - class MyUserSerializer < ActiveModel::Serializer - attributes :first_name, :last_name - - def serializable_hash - hash = attributes - hash = hash.merge(:super_user => true) if my_user.super_user? - hash - end - end - - class CommentSerializer - def initialize(comment, scope) - @comment, @scope = comment, scope - end - - def serializable_hash - { :title => @comment.read_attribute_for_serialization(:title) } - end - - def as_json - { :comment => serializable_hash } - end - end - - class PostSerializer < ActiveModel::Serializer - attributes :title, :body - has_many :comments, :serializer => CommentSerializer - end - - def test_attributes - user = User.new - user_serializer = DefaultUserSerializer.new(user, {}) - - hash = user_serializer.as_json - - assert_equal({ - :default_user => { :first_name => "Jose", :last_name => "Valim" } - }, hash) - end - - def test_attributes_method - user = User.new - user_serializer = UserSerializer.new(user, {}) - - hash = user_serializer.as_json - - assert_equal({ - :user => { :first_name => "Jose", :last_name => "Valim", :ok => true } - }, hash) - end - - def test_serializer_receives_scope - user = User.new - user_serializer = UserSerializer.new(user, {:scope => true}) - - hash = user_serializer.as_json - - assert_equal({ - :user => { - :first_name => "Jose", - :last_name => "Valim", - :ok => true, - :scope => true - } - }, hash) - end - - def test_pretty_accessors - user = User.new - user.superuser = true - user_serializer = MyUserSerializer.new(user, nil) - - hash = user_serializer.as_json - - assert_equal({ - :my_user => { - :first_name => "Jose", :last_name => "Valim", :super_user => true - } - }, hash) - end - - def test_has_many - user = User.new - - post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com") - comments = [Comment.new(:title => "Comment1"), Comment.new(:title => "Comment2")] - post.comments = comments - - post_serializer = PostSerializer.new(post, user) - - assert_equal({ - :post => { - :title => "New Post", - :body => "Body of new post", - :comments => [ - { :title => "Comment1" }, - { :title => "Comment2" } - ] - } - }, post_serializer.as_json) - end - - class Blog < Model - attr_accessor :author - end - - class AuthorSerializer < ActiveModel::Serializer - attributes :first_name, :last_name - end - - class BlogSerializer < ActiveModel::Serializer - has_one :author, :serializer => AuthorSerializer - end - - def test_has_one - user = User.new - blog = Blog.new - blog.author = user - - json = BlogSerializer.new(blog, user).as_json - assert_equal({ - :blog => { - :author => { - :first_name => "Jose", - :last_name => "Valim" - } - } - }, json) - end - - def test_implicit_serializer - author_serializer = Class.new(ActiveModel::Serializer) do - attributes :first_name - end - - blog_serializer = Class.new(ActiveModel::Serializer) do - const_set(:AuthorSerializer, author_serializer) - has_one :author - end - - user = User.new - blog = Blog.new - blog.author = user - - json = blog_serializer.new(blog, user).as_json - assert_equal({ - :author => { - :first_name => "Jose" - } - }, json) - end - - def test_overridden_associations - author_serializer = Class.new(ActiveModel::Serializer) do - attributes :first_name - end - - blog_serializer = Class.new(ActiveModel::Serializer) do - const_set(:PersonSerializer, author_serializer) - - def person - object.author - end - - has_one :person - end - - user = User.new - blog = Blog.new - blog.author = user - - json = blog_serializer.new(blog, user).as_json - assert_equal({ - :person => { - :first_name => "Jose" - } - }, json) - end - - def post_serializer(type) - Class.new(ActiveModel::Serializer) do - attributes :title, :body - has_many :comments, :serializer => CommentSerializer - - if type != :super - define_method :serializable_hash do - post_hash = attributes - post_hash.merge!(send(type)) - post_hash - end - end - end - end - - def test_associations - post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com") - comments = [Comment.new(:title => "Comment1"), Comment.new(:title => "Comment2")] - post.comments = comments - - serializer = post_serializer(:associations).new(post, nil) - - assert_equal({ - :title => "New Post", - :body => "Body of new post", - :comments => [ - { :title => "Comment1" }, - { :title => "Comment2" } - ] - }, serializer.as_json) - end - - def test_association_ids - serializer = post_serializer(:association_ids) - - serializer.class_eval do - def as_json(*) - { :post => serializable_hash }.merge(associations) - end - end - - post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com") - comments = [Comment.new(:title => "Comment1", :id => 1), Comment.new(:title => "Comment2", :id => 2)] - post.comments = comments - - serializer = serializer.new(post, nil) - - assert_equal({ - :post => { - :title => "New Post", - :body => "Body of new post", - :comments => [1, 2] - }, - :comments => [ - { :title => "Comment1" }, - { :title => "Comment2" } - ] - }, serializer.as_json) - end - - def test_associations_with_nil_association - user = User.new - blog = Blog.new - - json = BlogSerializer.new(blog, user).as_json - assert_equal({ - :blog => { :author => nil } - }, json) - - serializer = Class.new(BlogSerializer) do - root :blog - - def serializable_hash - attributes.merge(association_ids) - end - end - - json = serializer.new(blog, user).as_json - assert_equal({ :blog => { :author => nil } }, json) - end - - def test_custom_root - user = User.new - blog = Blog.new - - serializer = Class.new(BlogSerializer) do - root :my_blog - end - - assert_equal({ :my_blog => { :author => nil } }, serializer.new(blog, user).as_json) - end - - def test_false_root - user = User.new - blog = Blog.new - - serializer = Class.new(BlogSerializer) do - root false - end - - assert_equal({ :author => nil }, serializer.new(blog, user).as_json) - - # test inherited false root - serializer = Class.new(serializer) - assert_equal({ :author => nil }, serializer.new(blog, user).as_json) - end - - def test_embed_ids - serializer = post_serializer(:super) - - serializer.class_eval do - root :post - embed :ids - end - - post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com") - comments = [Comment.new(:title => "Comment1", :id => 1), Comment.new(:title => "Comment2", :id => 2)] - post.comments = comments - - serializer = serializer.new(post, nil) - - assert_equal({ - :post => { - :title => "New Post", - :body => "Body of new post", - :comments => [1, 2] - } - }, serializer.as_json) - end - - def test_embed_ids_include_true - serializer = post_serializer(:super) - - serializer.class_eval do - root :post - embed :ids, :include => true - end - - post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com") - comments = [Comment.new(:title => "Comment1", :id => 1), Comment.new(:title => "Comment2", :id => 2)] - post.comments = comments - - serializer = serializer.new(post, nil) - - assert_equal({ - :post => { - :title => "New Post", - :body => "Body of new post", - :comments => [1, 2] - }, - :comments => [ - { :title => "Comment1" }, - { :title => "Comment2" } - ] - }, serializer.as_json) - end - - def test_embed_objects - serializer = post_serializer(:super) - - serializer.class_eval do - root :post - embed :objects - end - - post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com") - comments = [Comment.new(:title => "Comment1", :id => 1), Comment.new(:title => "Comment2", :id => 2)] - post.comments = comments - - serializer = serializer.new(post, nil) - - assert_equal({ - :post => { - :title => "New Post", - :body => "Body of new post", - :comments => [ - { :title => "Comment1" }, - { :title => "Comment2" } - ] - } - }, serializer.as_json) - end - - def test_array_serializer - model = Model.new - user = User.new - comments = Comment.new(:title => "Comment1", :id => 1) - - array = [model, user, comments] - serializer = array.active_model_serializer.new(array, {:scope => true}) - assert_equal([ - { :model => "Model" }, - { :user => { :last_name=>"Valim", :ok=>true, :first_name=>"Jose", :scope => true } }, - { :comment => { :title => "Comment1" } } - ], serializer.as_json) - end -end
\ No newline at end of file diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 1b89bfa6f9..6b0be4c096 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -7,8 +7,6 @@ Example: config.railties_order = [Blog::Engine, :main_app, :all] -* Add a serializer generator and add a hook for it in the scaffold generators *José Valim* - * Scaffold returns 204 No Content for API requests without content. This makes scaffold work with jQuery out of the box. *José Valim* * Update Rails::Rack::Logger middleware to apply any tags set in config.log_tags to the newly ActiveSupport::TaggedLogging Rails.logger. This makes it easy to tag log lines with debug information like subdomain and request id -- both very helpful in debugging multi-user production applications *DHH* diff --git a/railties/guides/source/serializers.textile b/railties/guides/source/serializers.textile deleted file mode 100644 index efc7cbf248..0000000000 --- a/railties/guides/source/serializers.textile +++ /dev/null @@ -1,563 +0,0 @@ -h2. Rails Serializers - -This guide describes how to use Active Model serializers to build non-trivial JSON services in Rails. By reading this guide, you will learn: - -* When to use the built-in Active Model serialization -* When to use a custom serializer for your models -* How to use serializers to encapsulate authorization concerns -* How to create serializer templates to describe the application-wide structure of your serialized JSON -* How to build resources not backed by a single database table for use with JSON services - -This guide covers an intermediate topic and assumes familiarity with Rails conventions. It is suitable for applications that expose a -JSON API that may return different results based on the authorization status of the user. - -endprologue. - -h3. Serialization - -By default, Active Record objects can serialize themselves into JSON by using the `to_json` method. This method takes a series of additional -parameter to control which properties and associations Rails should include in the serialized output. - -When building a web application that uses JavaScript to retrieve JSON data from the server, this mechanism has historically been the primary -way that Rails developers prepared their responses. This works great for simple cases, as the logic for serializing an Active Record object -is neatly encapsulated in Active Record itself. - -However, this solution quickly falls apart in the face of serialization requirements based on authorization. For instance, a web service -may choose to expose additional information about a resource only if the user is entitled to access it. In addition, a JavaScript front-end -may want information that is not neatly described in terms of serializing a single Active Record object, or in a different format than. - -In addition, neither the controller nor the model seems like the correct place for logic that describes how to serialize an model object -*for the current user*. - -Serializers solve these problems by encapsulating serialization in an object designed for this purpose. If the default +to_json+ semantics, -with at most a few configuration options serve your needs, by all means continue to use the built-in +to_json+. If you find yourself doing -hash-driven-development in your controllers, juggling authorization logic and other concerns, serializers are for you! - -h3. The Most Basic Serializer - -A basic serializer is a simple Ruby object named after the model class it is serializing. - -<ruby> -class PostSerializer - def initialize(post, scope) - @post, @scope = post, scope - end - - def as_json - { post: { title: @post.name, body: @post.body } } - end -end -</ruby> - -A serializer is initialized with two parameters: the model object it should serialize and an authorization scope. By default, the -authorization scope is the current user (+current_user+) but you can use a different object if you want. The serializer also -implements an +as_json+ method, which returns a Hash that will be sent to the JSON encoder. - -Rails will transparently use your serializer when you use +render :json+ in your controller. - -<ruby> -class PostsController < ApplicationController - def show - @post = Post.find(params[:id]) - render json: @post - end -end -</ruby> - -Because +respond_with+ uses +render :json+ under the hood for JSON requests, Rails will automatically use your serializer when -you use +respond_with+ as well. - -h4. +serializable_hash+ - -In general, you will want to implement +serializable_hash+ and +as_json+ to allow serializers to embed associated content -directly. The easiest way to implement these two methods is to have +as_json+ call +serializable_hash+ and insert the root. - -<ruby> -class PostSerializer - def initialize(post, scope) - @post, @scope = post, scope - end - - def serializable_hash - { title: @post.name, body: @post.body } - end - - def as_json - { post: serializable_hash } - end -end -</ruby> - -h4. Authorization - -Let's update our serializer to include the email address of the author of the post, but only if the current user has superuser -access. - -<ruby> -class PostSerializer - def initialize(post, scope) - @post, @scope = post, scope - end - - def as_json - { post: serializable_hash } - end - - def serializable_hash - hash = post - hash.merge!(super_data) if super? - hash - end - -private - def post - { title: @post.name, body: @post.body } - end - - def super_data - { email: @post.email } - end - - def super? - @scope.superuser? - end -end -</ruby> - -h4. Testing - -One benefit of encapsulating our objects this way is that it becomes extremely straight-forward to test the serialization -logic in isolation. - -<ruby> -require "ostruct" - -class PostSerializerTest < ActiveSupport::TestCase - # For now, we use a very simple authorization structure. These tests will need - # refactoring if we change that. - plebe = OpenStruct.new(super?: false) - god = OpenStruct.new(super?: true) - - post = OpenStruct.new(title: "Welcome to my blog!", body: "Blah blah blah", email: "tenderlove@gmail.com") - - test "a regular user sees just the title and body" do - json = PostSerializer.new(post, plebe).to_json - hash = JSON.parse(json) - - assert_equal post.title, hash.delete("title") - assert_equal post.body, hash.delete("body") - assert_empty hash - end - - test "a superuser sees the title, body and email" do - json = PostSerializer.new(post, god).to_json - hash = JSON.parse(json) - - assert_equal post.title, hash.delete("title") - assert_equal post.body, hash.delete("body") - assert_equal post.email, hash.delete("email") - assert_empty hash - end -end -</ruby> - -It's important to note that serializer objects define a clear interface specifically for serializing an existing object. -In this case, the serializer expects to receive a post object with +name+, +body+ and +email+ attributes and an authorization -scope with a +super?+ method. - -By defining a clear interface, it's must easier to ensure that your authorization logic is behaving correctly. In this case, -the serializer doesn't need to concern itself with how the authorization scope decides whether to set the +super?+ flag, just -whether it is set. In general, you should document these requirements in your serializer files and programatically via tests. -The documentation library +YARD+ provides excellent tools for describing this kind of requirement: - -<ruby> -class PostSerializer - # @param [~body, ~title, ~email] post the post to serialize - # @param [~super] scope the authorization scope for this serializer - def initialize(post, scope) - @post, @scope = post, scope - end - - # ... -end -</ruby> - -h3. Attribute Sugar - -To simplify this process for a number of common cases, Rails provides a default superclass named +ActiveModel::Serializer+ -that you can use to implement your serializers. - -For example, you will sometimes want to simply include a number of existing attributes from the source model into the outputted -JSON. In the above example, the +title+ and +body+ attributes were always included in the JSON. Let's see how to use -+ActiveModel::Serializer+ to simplify our post serializer. - -<ruby> -class PostSerializer < ActiveModel::Serializer - attributes :title, :body - - def initialize(post, scope) - @post, @scope = post, scope - end - - def serializable_hash - hash = attributes - hash.merge!(super_data) if super? - hash - end - -private - def super_data - { email: @post.email } - end - - def super? - @scope.superuser? - end -end -</ruby> - -First, we specified the list of included attributes at the top of the class. This will create an instance method called -+attributes+ that extracts those attributes from the post model. - -NOTE: Internally, +ActiveModel::Serializer+ uses +read_attribute_for_serialization+, which defaults to +read_attribute+, which defaults to +send+. So if you're rolling your own models for use with the serializer, you can use simple Ruby accessors for your attributes if you like. - -Next, we use the attributes methood in our +serializable_hash+ method, which allowed us to eliminate the +post+ method we hand-rolled -earlier. We could also eliminate the +as_json+ method, as +ActiveModel::Serializer+ provides a default +as_json+ method for -us that calls our +serializable_hash+ method and inserts a root. But we can go a step further! - -<ruby> -class PostSerializer < ActiveModel::Serializer - attributes :title, :body - -private - def attributes - hash = super - hash.merge!(email: post.email) if super? - hash - end - - def super? - @scope.superuser? - end -end -</ruby> - -The superclass provides a default +initialize+ method as well as a default +serializable_hash+ method, which uses -+attributes+. We can call +super+ to get the hash based on the attributes we declared, and then add in any additional -attributes we want to use. - -NOTE: +ActiveModel::Serializer+ will create an accessor matching the name of the current class for the resource you pass in. In this case, because we have defined a PostSerializer, we can access the resource with the +post+ accessor. - -h3. Associations - -In most JSON APIs, you will want to include associated objects with your serialized object. In this case, let's include -the comments with the current post. - -<ruby> -class PostSerializer < ActiveModel::Serializer - attributes :title, :body - has_many :comments - -private - def attributes - hash = super - hash.merge!(email: post.email) if super? - hash - end - - def super? - @scope.superuser? - end -end -</ruby> - -The default +serializable_hash+ method will include the comments as embedded objects inside the post. - -<javascript> -{ - post: { - title: "Hello Blog!", - body: "This is my first post. Isn't it fabulous!", - comments: [ - { - title: "Awesome", - body: "Your first post is great" - } - ] - } -} -</javascript> - -Rails uses the same logic to generate embedded serializations as it does when you use +render :json+. In this case, -because you didn't define a +CommentSerializer+, Rails used the default +as_json+ on your comment object. - -If you define a serializer, Rails will automatically instantiate it with the existing authorization scope. - -<ruby> -class CommentSerializer - def initialize(comment, scope) - @comment, @scope = comment, scope - end - - def serializable_hash - { title: @comment.title } - end - - def as_json - { comment: serializable_hash } - end -end -</ruby> - -If we define the above comment serializer, the outputted JSON will change to: - -<javascript> -{ - post: { - title: "Hello Blog!", - body: "This is my first post. Isn't it fabulous!", - comments: [{ title: "Awesome" }] - } -} -</javascript> - -Let's imagine that our comment system allows an administrator to kill a comment, and we only want to allow -users to see the comments they're entitled to see. By default, +has_many :comments+ will simply use the -+comments+ accessor on the post object. We can override the +comments+ accessor to limit the comments used -to just the comments we want to allow for the current user. - -<ruby> -class PostSerializer < ActiveModel::Serializer - attributes :title. :body - has_many :comments - -private - def attributes - hash = super - hash.merge!(email: post.email) if super? - hash - end - - def comments - post.comments_for(scope) - end - - def super? - @scope.superuser? - end -end -</ruby> - -+ActiveModel::Serializer+ will still embed the comments, but this time it will use just the comments -for the current user. - -NOTE: The logic for deciding which comments a user should see still belongs in the model layer. In general, you should encapsulate concerns that require making direct Active Record queries in scopes or public methods on your models. - -h3. Customizing Associations - -Not all front-ends expect embedded documents in the same form. In these cases, you can override the -default +serializable_hash+, and use conveniences provided by +ActiveModel::Serializer+ to avoid having to -build up the hash manually. - -For example, let's say our front-end expects the posts and comments in the following format: - -<plain> -{ - post: { - id: 1 - title: "Hello Blog!", - body: "This is my first post. Isn't it fabulous!", - comments: [1,2] - }, - comments: [ - { - id: 1 - title: "Awesome", - body: "Your first post is great" - }, - { - id: 2 - title: "Not so awesome", - body: "Why is it so short!" - } - ] -} -</plain> - -We could achieve this with a custom +as_json+ method. We will also need to define a serializer for comments. - -<ruby> -class CommentSerializer < ActiveModel::Serializer - attributes :id, :title, :body - - # define any logic for dealing with authorization-based attributes here -end - -class PostSerializer < ActiveModel::Serializer - attributes :title, :body - has_many :comments - - def as_json - { post: serializable_hash }.merge!(associations) - end - - def serializable_hash - post_hash = attributes - post_hash.merge!(association_ids) - post_hash - end - -private - def attributes - hash = super - hash.merge!(email: post.email) if super? - hash - end - - def comments - post.comments_for(scope) - end - - def super? - @scope.superuser? - end -end -</ruby> - -Here, we used two convenience methods: +associations+ and +association_ids+. The first, -+associations+, creates a hash of all of the define associations, using their defined -serializers. The second, +association_ids+, generates a hash whose key is the association -name and whose value is an Array of the association's keys. - -The +association_ids+ helper will use the overridden version of the association, so in -this case, +association_ids+ will only include the ids of the comments provided by the -+comments+ method. - -h3. Special Association Serializers - -So far, associations defined in serializers use either the +as_json+ method on the model -or the defined serializer for the association type. Sometimes, you may want to serialize -associated models differently when they are requested as part of another resource than -when they are requested on their own. - -For instance, we might want to provide the full comment when it is requested directly, -but only its title when requested as part of the post. To achieve this, you can define -a serializer for associated objects nested inside the main serializer. - -<ruby> -class PostSerializer < ActiveModel::Serializer - class CommentSerializer < ActiveModel::Serializer - attributes :id, :title - end - - # same as before - # ... -end -</ruby> - -In other words, if a +PostSerializer+ is trying to serialize comments, it will first -look for +PostSerializer::CommentSerializer+ before falling back to +CommentSerializer+ -and finally +comment.as_json+. - -h3. Overriding the Defaults - -h4. Authorization Scope - -By default, the authorization scope for serializers is +:current_user+. This means -that when you call +render json: @post+, the controller will automatically call -its +current_user+ method and pass that along to the serializer's initializer. - -If you want to change that behavior, simply use the +serialization_scope+ class -method. - -<ruby> -class PostsController < ApplicationController - serialization_scope :current_app -end -</ruby> - -You can also implement an instance method called (no surprise) +serialization_scope+, -which allows you to define a dynamic authorization scope based on the current request. - -WARNING: If you use different objects as authorization scopes, make sure that they all implement whatever interface you use in your serializers to control what the outputted JSON looks like. - -h3. Using Serializers Outside of a Request - -The serialization API encapsulates the concern of generating a JSON representation of -a particular model for a particular user. As a result, you should be able to easily use -serializers, whether you define them yourself or whether you use +ActiveModel::Serializer+ -outside a request. - -For instance, if you want to generate the JSON representation of a post for a user outside -of a request: - -<ruby> -user = get_user # some logic to get the user in question -PostSerializer.new(post, user).to_json # reliably generate JSON output -</ruby> - -If you want to generate JSON for an anonymous user, you should be able to use whatever -technique you use in your application to generate anonymous users outside of a request. -Typically, that means creating a new user and not saving it to the database: - -<ruby> -user = User.new # create a new anonymous user -PostSerializer.new(post, user).to_json -</ruby> - -In general, the better you encapsulate your authorization logic, the more easily you -will be able to use the serializer outside of the context of a request. For instance, -if you use an authorization library like Cancan, which uses a uniform +user.can?(action, model)+, -the authorization interface can very easily be replaced by a plain Ruby object for -testing or usage outside the context of a request. - -h3. Collections - -So far, we've talked about serializing individual model objects. By default, Rails -will serialize collections, including when using the +associations+ helper, by -looping over each element of the collection, calling +serializable_hash+ on the element, -and then grouping them by their type (using the plural version of their class name -as the root). - -For example, an Array of post objects would serialize as: - -<plain> -{ - posts: [ - { - title: "FIRST POST!", - body: "It's my first pooooost" - }, - { title: "Second post!", - body: "Zomg I made it to my second post" - } - ] -} -</plain> - -If you want to change the behavior of serialized Arrays, you need to create -a custom Array serializer. - -<ruby> -class ArraySerializer < ActiveModel::ArraySerializer - def serializable_array - serializers.map do |serializer| - serializer.serializable_hash - end - end - - def as_json - hash = { root => serializable_array } - hash.merge!(associations) - hash - end -end -</ruby> - -When generating embedded associations using the +associations+ helper inside a -regular serializer, it will create a new <code>ArraySerializer</code> with the -associated content and call its +serializable_array+ method. In this case, those -embedded associations will not recursively include associations. - -When generating an Array using +render json: posts+, the controller will invoke -the +as_json+ method, which will include its associations and its root. diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index f1ca9080ff..27f8d13ce8 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -33,8 +33,7 @@ module Rails :stylesheets => '-y', :stylesheet_engine => '-se', :template_engine => '-e', - :test_framework => '-t', - :serializer => '-z' + :test_framework => '-t' }, :test_unit => { @@ -59,7 +58,6 @@ module Rails :performance_tool => nil, :resource_controller => :controller, :scaffold_controller => :scaffold_controller, - :serializer => false, :stylesheets => true, :stylesheet_engine => :css, :test_framework => false, diff --git a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb index 7353a67c83..03a61a035e 100644 --- a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb @@ -10,7 +10,6 @@ module Rails class_option :stylesheet_engine, :desc => "Engine for Stylesheets" hook_for :scaffold_controller, :required => true - hook_for :serializer hook_for :assets do |assets| invoke assets, [controller_name] diff --git a/railties/lib/rails/generators/rails/serializer/USAGE b/railties/lib/rails/generators/rails/serializer/USAGE deleted file mode 100644 index a49f7ea1fb..0000000000 --- a/railties/lib/rails/generators/rails/serializer/USAGE +++ /dev/null @@ -1,9 +0,0 @@ -Description: - Generates a serializer for the given resource with tests. - -Example: - `rails generate serializer Account name created_at` - - For TestUnit it creates: - Serializer: app/serializers/account_serializer.rb - TestUnit: test/unit/account_serializer_test.rb diff --git a/railties/lib/rails/generators/rails/serializer/serializer_generator.rb b/railties/lib/rails/generators/rails/serializer/serializer_generator.rb deleted file mode 100644 index 2118906227..0000000000 --- a/railties/lib/rails/generators/rails/serializer/serializer_generator.rb +++ /dev/null @@ -1,39 +0,0 @@ -module Rails - module Generators - class SerializerGenerator < NamedBase - check_class_collision :suffix => "Serializer" - - argument :attributes, :type => :array, :default => [], :banner => "field:type field:type" - - class_option :parent, :type => :string, :desc => "The parent class for the generated serializer" - - def create_serializer_file - template 'serializer.rb', File.join('app/serializers', class_path, "#{file_name}_serializer.rb") - end - - hook_for :test_framework - - private - - def attributes_names - attributes.select { |attr| !attr.reference? }.map { |a| a.name.to_sym } - end - - def association_names - attributes.select { |attr| attr.reference? }.map { |a| a.name.to_sym } - end - - def parent_class_name - if options[:parent] - options[:parent] - elsif (n = Rails::Generators.namespace) && n.const_defined?(:ApplicationSerializer) - "ApplicationSerializer" - elsif Object.const_defined?(:ApplicationSerializer) - "ApplicationSerializer" - else - "ActiveModel::Serializer" - end - end - end - end -end diff --git a/railties/lib/rails/generators/rails/serializer/templates/serializer.rb b/railties/lib/rails/generators/rails/serializer/templates/serializer.rb deleted file mode 100644 index 30c058c7e9..0000000000 --- a/railties/lib/rails/generators/rails/serializer/templates/serializer.rb +++ /dev/null @@ -1,9 +0,0 @@ -<% module_namespacing do -%> -class <%= class_name %>Serializer < <%= parent_class_name %> -<% if attributes.any? -%> attributes <%= attributes_names.map(&:inspect).join(", ") %> -<% end -%> -<% association_names.each do |attribute| -%> - has_one :<%= attribute %> -<% end -%> -end -<% end -%>
\ No newline at end of file diff --git a/railties/lib/rails/generators/test_unit/serializer/serializer_generator.rb b/railties/lib/rails/generators/test_unit/serializer/serializer_generator.rb deleted file mode 100644 index 533c032c3b..0000000000 --- a/railties/lib/rails/generators/test_unit/serializer/serializer_generator.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'rails/generators/test_unit' - -module TestUnit - module Generators - class SerializerGenerator < Base - check_class_collision :suffix => "SerializerTest" - - def create_test_files - template 'unit_test.rb', File.join('test/unit', class_path, "#{file_name}_serializer_test.rb") - end - end - end -end diff --git a/railties/lib/rails/generators/test_unit/serializer/templates/unit_test.rb b/railties/lib/rails/generators/test_unit/serializer/templates/unit_test.rb deleted file mode 100644 index 0b1bbdcaa5..0000000000 --- a/railties/lib/rails/generators/test_unit/serializer/templates/unit_test.rb +++ /dev/null @@ -1,9 +0,0 @@ -require 'test_helper' - -<% module_namespacing do -%> -class <%= class_name %>SerializerTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end -<% end -%> diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index 2e8b03fbef..2db8090621 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -264,15 +264,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_file "app/assets/stylesheets/posts.css" end - def test_scaffold_also_generators_serializer - run_generator [ "posts", "name:string", "author:references", "--serializer" ] - assert_file "app/serializers/post_serializer.rb" do |serializer| - assert_match /class PostSerializer < ActiveModel::Serializer/, serializer - assert_match /^ attributes :name$/, serializer - assert_match /^ has_one :author$/, serializer - end - end - def test_scaffold_generator_outputs_error_message_on_missing_attribute_type run_generator ["post", "title", "body:text", "author"] diff --git a/railties/test/generators/serializer_generator_test.rb b/railties/test/generators/serializer_generator_test.rb deleted file mode 100644 index 2afaa65693..0000000000 --- a/railties/test/generators/serializer_generator_test.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/serializer/serializer_generator' - -class SerializerGeneratorTest < Rails::Generators::TestCase - include GeneratorsTestHelper - arguments %w(account name:string description:text business:references) - - def test_generates_a_serializer - run_generator - assert_file "app/serializers/account_serializer.rb", /class AccountSerializer < ActiveModel::Serializer/ - end - - def test_generates_a_namespaced_serializer - run_generator ["admin/account"] - assert_file "app/serializers/admin/account_serializer.rb", /class Admin::AccountSerializer < ActiveModel::Serializer/ - end - - def test_uses_application_serializer_if_one_exists - Object.const_set(:ApplicationSerializer, Class.new) - run_generator - assert_file "app/serializers/account_serializer.rb", /class AccountSerializer < ApplicationSerializer/ - ensure - Object.send :remove_const, :ApplicationSerializer - end - - def test_uses_namespace_application_serializer_if_one_exists - Object.const_set(:SerializerNamespace, Module.new) - SerializerNamespace.const_set(:ApplicationSerializer, Class.new) - Rails::Generators.namespace = SerializerNamespace - run_generator - assert_file "app/serializers/serializer_namespace/account_serializer.rb", - /module SerializerNamespace\n class AccountSerializer < ApplicationSerializer/ - ensure - Object.send :remove_const, :SerializerNamespace - Rails::Generators.namespace = nil - end - - def test_uses_given_parent - Object.const_set(:ApplicationSerializer, Class.new) - run_generator ["Account", "--parent=MySerializer"] - assert_file "app/serializers/account_serializer.rb", /class AccountSerializer < MySerializer/ - ensure - Object.send :remove_const, :ApplicationSerializer - end - - def test_generates_attributes_and_associations - run_generator - assert_file "app/serializers/account_serializer.rb" do |serializer| - assert_match(/^ attributes :name, :description$/, serializer) - assert_match(/^ has_one :business$/, serializer) - end - end - - def test_with_no_attributes_does_not_add_extra_space - run_generator ["account"] - assert_file "app/serializers/account_serializer.rb", /class AccountSerializer < ActiveModel::Serializer\nend/ - end - - def test_invokes_default_test_framework - run_generator - assert_file "test/unit/account_serializer_test.rb", /class AccountSerializerTest < ActiveSupport::TestCase/ - end -end |