diff options
Diffstat (limited to 'railties')
11 files changed, 1 insertions, 720 deletions
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 |