diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..d87d4be66 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.gem +*.rbc +.bundle +.config +.yardoc +Gemfile.lock +InstalledFiles +_yardoc +coverage +doc/ +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp diff --git a/Gemfile b/Gemfile new file mode 100644 index 000000000..8845621d2 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'http://rubygems.org' + +# Specify your gem's dependencies in active_model_serializers.gemspec +gemspec diff --git a/README.textile b/README.textile new file mode 100644 index 000000000..e376227b5 --- /dev/null +++ b/README.textile @@ -0,0 +1,561 @@ +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. + +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. + + +class PostSerializer + def initialize(post, scope) + @post, @scope = post, scope + end + + def as_json + { post: { title: @post.name, body: @post.body } } + end +end + + +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. + + +class PostsController < ApplicationController + def show + @post = Post.find(params[:id]) + render json: @post + end +end + + +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. + + +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 + + +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. + + +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 + + +h4. Testing + +One benefit of encapsulating our objects this way is that it becomes extremely straight-forward to test the serialization +logic in isolation. + + +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 + + +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: + + +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 + + +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. + + +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 + + +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! + + +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 + + +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. + + +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 + + +The default +serializable_hash+ method will include the comments as embedded objects inside the post. + + +{ + post: { + title: "Hello Blog!", + body: "This is my first post. Isn't it fabulous!", + comments: [ + { + title: "Awesome", + body: "Your first post is great" + } + ] + } +} + + +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. + + +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 + + +If we define the above comment serializer, the outputted JSON will change to: + + +{ + post: { + title: "Hello Blog!", + body: "This is my first post. Isn't it fabulous!", + comments: [{ title: "Awesome" }] + } +} + + +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. + + +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 + + ++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: + + +{ + 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!" + } + ] +} + + +We could achieve this with a custom +as_json+ method. We will also need to define a serializer for comments. + + +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 + + +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. + + +class PostSerializer < ActiveModel::Serializer + class CommentSerializer < ActiveModel::Serializer + attributes :id, :title + end + + # same as before + # ... +end + + +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. + + +class PostsController < ApplicationController + serialization_scope :current_app +end + + +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: + + +user = get_user # some logic to get the user in question +PostSerializer.new(post, user).to_json # reliably generate JSON output + + +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: + + +user = User.new # create a new anonymous user +PostSerializer.new(post, user).to_json + + +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: + + +{ + posts: [ + { + title: "FIRST POST!", + body: "It's my first pooooost" + }, + { title: "Second post!", + body: "Zomg I made it to my second post" + } + ] +} + + +If you want to change the behavior of serialized Arrays, you need to create +a custom Array serializer. + + +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 + + +When generating embedded associations using the +associations+ helper inside a +regular serializer, it will create a new ArraySerializer 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/Rakefile b/Rakefile new file mode 100644 index 000000000..f57ae68a8 --- /dev/null +++ b/Rakefile @@ -0,0 +1,2 @@ +#!/usr/bin/env rake +require "bundler/gem_tasks" diff --git a/active_model_serializers.gemspec b/active_model_serializers.gemspec new file mode 100644 index 000000000..a6fa3dbc4 --- /dev/null +++ b/active_model_serializers.gemspec @@ -0,0 +1,17 @@ +# -*- encoding: utf-8 -*- +Gem::Specification.new do |gem| + gem.authors = ["José Valim"] + gem.email = ["jose.valim@gmail.com"] + gem.description = %q{TODO: Write a gem description} + gem.summary = %q{TODO: Write a gem summary} + gem.homepage = "" + + gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } + gem.files = `git ls-files`.split("\n") + gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") + gem.name = "active_model_serializers" + gem.require_paths = ["lib"] + gem.version = "0.0.1" + + gem.add_dependency "rails", "~> 3.0" +end diff --git a/lib/action_controller/serialization.rb b/lib/action_controller/serialization.rb new file mode 100644 index 000000000..7a1e4c911 --- /dev/null +++ b/lib/action_controller/serialization.rb @@ -0,0 +1,51 @@ +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 \ No newline at end of file diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb new file mode 100644 index 000000000..0e23df2f2 --- /dev/null +++ b/lib/active_model/serializer.rb @@ -0,0 +1,253 @@ +require "active_support/core_ext/class/attribute" +require "active_support/core_ext/string/inflections" +require "active_support/core_ext/module/anonymous" +require "set" + +module ActiveModel + # Active Model Array Serializer + # + # It serializes an array checking if each element that implements + # the +active_model_serializer+ method passing down the current scope. + class ArraySerializer + attr_reader :object, :scope + + def initialize(object, scope) + @object, @scope = object, scope + end + + def serializable_array + @object.map do |item| + if item.respond_to?(:active_model_serializer) && (serializer = item.active_model_serializer) + serializer.new(item, scope) + else + item + end + end + end + + def as_json(*args) + serializable_array.as_json(*args) + end + end + + # Active Model Serializer + # + # Provides a basic serializer implementation that allows you to easily + # control how a given object is going to be serialized. On initialization, + # it expects to object as arguments, a resource and a scope. For example, + # one may do in a controller: + # + # PostSerializer.new(@post, current_user).to_json + # + # The object to be serialized is the +@post+ and the scope is +current_user+. + # + # We use the scope to check if a given attribute should be serialized or not. + # For example, some attributes maybe only be returned if +current_user+ is the + # author of the post: + # + # class PostSerializer < ActiveModel::Serializer + # attributes :title, :body + # has_many :comments + # + # private + # + # def attributes + # hash = super + # hash.merge!(:email => post.email) if author? + # hash + # end + # + # def author? + # post.author == scope + # end + # end + # + class Serializer + module Associations #:nodoc: + class Config < Struct.new(:name, :options) #:nodoc: + def serializer + options[:serializer] + end + end + + class HasMany < Config #:nodoc: + def serialize(collection, scope) + collection.map do |item| + serializer.new(item, scope).serializable_hash + end + end + + def serialize_ids(collection, scope) + # use named scopes if they are present + # return collection.ids if collection.respond_to?(:ids) + + collection.map do |item| + item.read_attribute_for_serialization(:id) + end + end + end + + class HasOne < Config #:nodoc: + def serialize(object, scope) + object && serializer.new(object, scope).serializable_hash + end + + def serialize_ids(object, scope) + object && object.read_attribute_for_serialization(:id) + end + end + end + + class_attribute :_attributes + self._attributes = Set.new + + class_attribute :_associations + self._associations = [] + + class_attribute :_root + class_attribute :_embed + self._embed = :objects + class_attribute :_root_embed + + class << self + # Define attributes to be used in the serialization. + def attributes(*attrs) + self._attributes += attrs + end + + def associate(klass, attrs) #:nodoc: + options = attrs.extract_options! + self._associations += attrs.map do |attr| + unless method_defined?(attr) + class_eval "def #{attr}() object.#{attr} end", __FILE__, __LINE__ + end + + options[:serializer] ||= const_get("#{attr.to_s.camelize}Serializer") + klass.new(attr, options) + end + end + + # Defines an association in the object should be rendered. + # + # The serializer object should implement the association name + # as a method which should return an array when invoked. If a method + # with the association name does not exist, the association name is + # dispatched to the serialized object. + def has_many(*attrs) + associate(Associations::HasMany, attrs) + end + + # Defines an association in the object should be rendered. + # + # The serializer object should implement the association name + # as a method which should return an object when invoked. If a method + # with the association name does not exist, the association name is + # dispatched to the serialized object. + def has_one(*attrs) + associate(Associations::HasOne, attrs) + end + + # Define how associations should be embedded. + # + # embed :objects # Embed associations as full objects + # embed :ids # Embed only the association ids + # embed :ids, :include => true # Embed the association ids and include objects in the root + # + def embed(type, options={}) + self._embed = type + self._root_embed = true if options[:include] + end + + # Defines the root used on serialization. If false, disables the root. + def root(name) + self._root = name + end + + def inherited(klass) #:nodoc: + return if klass.anonymous? + + name = klass.name.demodulize.underscore.sub(/_serializer$/, '') + + klass.class_eval do + alias_method name.to_sym, :object + root name.to_sym unless self._root == false + end + end + end + + attr_reader :object, :scope + + def initialize(object, scope) + @object, @scope = object, scope + end + + # Returns a json representation of the serializable + # object including the root. + def as_json(*) + if _root + hash = { _root => serializable_hash } + hash.merge!(associations) if _root_embed + hash + else + serializable_hash + end + end + + # Returns a hash representation of the serializable + # object without the root. + def serializable_hash + if _embed == :ids + attributes.merge(association_ids) + elsif _embed == :objects + attributes.merge(associations) + else + attributes + end + end + + # Returns a hash representation of the serializable + # object associations. + def associations + hash = {} + + _associations.each do |association| + associated_object = send(association.name) + hash[association.name] = association.serialize(associated_object, scope) + end + + hash + end + + # Returns a hash representation of the serializable + # object associations ids. + def association_ids + hash = {} + + _associations.each do |association| + associated_object = send(association.name) + hash[association.name] = association.serialize_ids(associated_object, scope) + end + + hash + end + + # Returns a hash representation of the serializable + # object attributes. + def attributes + hash = {} + + _attributes.each do |name| + hash[name] = @object.read_attribute_for_serialization(name) + end + + hash + end + end +end + +class Array + # Array uses ActiveModel::ArraySerializer. + def active_model_serializer + ActiveModel::ArraySerializer + end +end \ No newline at end of file diff --git a/lib/active_model_serializers.rb b/lib/active_model_serializers.rb new file mode 100644 index 000000000..a6c2cf99c --- /dev/null +++ b/lib/active_model_serializers.rb @@ -0,0 +1,28 @@ +require "active_model" +require "active_model/serializer" + +ActiveModel::Serialization.class_eval do + extend ActiveSupport::Concern + + 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 + + # Returns a model serializer for this object considering its namespace. + def active_model_serializer + self.class.active_model_serializer + end +end + +require "action_controller" + +module ActionController + autoload :Serialization, "action_controller/serialization" +end + +ActiveSupport.on_load(:action_controller) do + include ::ActionController::Serialization +end \ No newline at end of file diff --git a/lib/rails/generators/rails/serializer/USAGE b/lib/rails/generators/rails/serializer/USAGE new file mode 100644 index 000000000..a49f7ea1f --- /dev/null +++ b/lib/rails/generators/rails/serializer/USAGE @@ -0,0 +1,9 @@ +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/lib/rails/generators/rails/serializer/serializer_generator.rb b/lib/rails/generators/rails/serializer/serializer_generator.rb new file mode 100644 index 000000000..211890622 --- /dev/null +++ b/lib/rails/generators/rails/serializer/serializer_generator.rb @@ -0,0 +1,39 @@ +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/lib/rails/generators/rails/serializer/templates/serializer.rb b/lib/rails/generators/rails/serializer/templates/serializer.rb new file mode 100644 index 000000000..30c058c7e --- /dev/null +++ b/lib/rails/generators/rails/serializer/templates/serializer.rb @@ -0,0 +1,9 @@ +<% 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/lib/rails/generators/test_unit/serializer/serializer_generator.rb b/lib/rails/generators/test_unit/serializer/serializer_generator.rb new file mode 100644 index 000000000..533c032c3 --- /dev/null +++ b/lib/rails/generators/test_unit/serializer/serializer_generator.rb @@ -0,0 +1,13 @@ +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/lib/rails/generators/test_unit/serializer/templates/unit_test.rb b/lib/rails/generators/test_unit/serializer/templates/unit_test.rb new file mode 100644 index 000000000..0b1bbdcaa --- /dev/null +++ b/lib/rails/generators/test_unit/serializer/templates/unit_test.rb @@ -0,0 +1,9 @@ +require 'test_helper' + +<% module_namespacing do -%> +class <%= class_name %>SerializerTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end +<% end -%>