ActiveModel::Serializer implementation and Rails hooks
Go to file
Benjamin Fleischer ced317d827 Fix thor warning to set type: :boolean, was setting { banner: "" }
```
require "rails/generators/rails/model/model_generator"

module Rails
  module Generators
    class ResourceGenerator < ModelGenerator # :nodoc:
      include ResourceHelpers

      hook_for :resource_controller, required: true do |controller|
        invoke controller, [ controller_name, options[:actions] ]
      end

      class_option :actions, type: :array, banner: "ACTION ACTION", default: [],
                             desc: "Actions for the resource controller"

      hook_for :resource_route, required: true
    end
  end
end
```

```
 # .bundle/ruby/2.2.0/bundler/gems/rails-4c5f1bc9d45e/railties/lib/rails/generators/base.rb
      # Invoke a generator based on the value supplied by the user to the
      # given option named "name". A class option is created when this method
      # is invoked and you can set a hash to customize it.
      #
      # ==== Examples
      #
      #   module Rails::Generators
      #     class ControllerGenerator < Base
      #       hook_for :test_framework, aliases: "-t"
      #     end
      #   end
      #
      # The example above will create a test framework option and will invoke
      # a generator based on the user supplied value.
      #
      # For example, if the user invoke the controller generator as:
      #
      #   rails generate controller Account --test-framework=test_unit
      #
      # The controller generator will then try to invoke the following generators:
      #
      #   "rails:test_unit", "test_unit:controller", "test_unit"
      #
      # Notice that "rails:generators:test_unit" could be loaded as well, what
      # Rails looks for is the first and last parts of the namespace. This is what
      # allows any test framework to hook into Rails as long as it provides any
      # of the hooks above.
      #
      # ==== Options
      #
      # The first and last part used to find the generator to be invoked are
      # guessed based on class invokes hook_for, as noticed in the example above.
      # This can be customized with two options: :in and :as.
      #
      # Let's suppose you are creating a generator that needs to invoke the
      # controller generator from test unit. Your first attempt is:
      #
      #   class AwesomeGenerator < Rails::Generators::Base
      #     hook_for :test_framework
      #   end
      #
      # The lookup in this case for test_unit as input is:
      #
      #   "test_unit:awesome", "test_unit"
      #
      # Which is not the desired lookup. You can change it by providing the
      # :as option:
      #
      #   class AwesomeGenerator < Rails::Generators::Base
      #     hook_for :test_framework, as: :controller
      #   end
      #
      # And now it will look up at:
      #
      #   "test_unit:controller", "test_unit"
      #
      # Similarly, if you want it to also look up in the rails namespace, you
      # just need to provide the :in value:
      #
      #   class AwesomeGenerator < Rails::Generators::Base
      #     hook_for :test_framework, in: :rails, as: :controller
      #   end
      #
      # And the lookup is exactly the same as previously:
      #
      #   "rails:test_unit", "test_unit:controller", "test_unit"
      #
      # ==== Switches
      #
      # All hooks come with switches for user interface. If you do not want
      # to use any test framework, you can do:
      #
      #   rails generate controller Account --skip-test-framework
      #
      # Or similarly:
      #
      #   rails generate controller Account --no-test-framework
      #
      # ==== Boolean hooks
      #
      # In some cases, you may want to provide a boolean hook. For example, webrat
      # developers might want to have webrat available on controller generator.
      # This can be achieved as:
      #
      #   Rails::Generators::ControllerGenerator.hook_for :webrat, type: :boolean
      #
      # Then, if you want webrat to be invoked, just supply:
      #
      #   rails generate controller Account --webrat
      #
      # The hooks lookup is similar as above:
      #
      #   "rails:generators:webrat", "webrat:generators:controller", "webrat"
      #
      # ==== Custom invocations
      #
      # You can also supply a block to hook_for to customize how the hook is
      # going to be invoked. The block receives two arguments, an instance
      # of the current class and the class to be invoked.
      #
      # For example, in the resource generator, the controller should be invoked
      # with a pluralized class name. But by default it is invoked with the same
      # name as the resource generator, which is singular. To change this, we
      # can give a block to customize how the controller can be invoked.
      #
      #   hook_for :resource_controller do |instance, controller|
      #     instance.invoke controller, [ instance.name.pluralize ]
      #   end
      #
      def self.hook_for(*names, &block)
        options = names.extract_options!
        in_base = options.delete(:in) || base_name
        as_hook = options.delete(:as) || generator_name

        names.each do |name|
          unless class_options.key?(name)
            defaults = if options[:type] == :boolean
              {}
            elsif [true, false].include?(default_value_for_option(name, options))
              { banner: "" }
            else
              { desc: "#{name.to_s.humanize} to be invoked", banner: "NAME" }
            end

            class_option(name, defaults.merge!(options))
          end

          hooks[name] = [ in_base, as_hook ]
          invoke_from_option(name, options, &block)
        end
      end
```

```
  # .bundle/ruby/2.2.0/gems/thor-0.19.4/lib/thor/parser/option.rb:113:in `validate!'
    #   parse :foo => true
    #   #=> Option foo with default value true and type boolean
    #
    # The valid types are :boolean, :numeric, :hash, :array and :string. If none
    # is given a default type is assumed. This default type accepts arguments as
    # string (--foo=value) or booleans (just --foo).
    #
    # By default all options are optional, unless :required is given.
    def validate_default_type!
      default_type = case @default
      when nil
        return
      when TrueClass, FalseClass
        required? ? :string : :boolean
      when Numeric
        :numeric
      when Symbol
        :string
      when Hash, Array, String
        @default.class.name.downcase.to_sym
      end

      # TODO: This should raise an ArgumentError in a future version of Thor
      if default_type != @type
        warn "Expected #{@type} default value for '#{switch_name}'; got #{@default.inspect} (#{default_type})"
      end
    end
```
2017-01-06 17:16:57 -06:00
.github Fix GitHub template formatting and spelling 2016-02-26 11:24:57 -07:00
bin Merge pull request #1588 from bf4/benchmark_revision_runner 2016-03-27 11:24:07 +02:00
docs Promote important architecture description that answers a lot of questions we get 2016-12-04 15:26:44 -06:00
lib Fix thor warning to set type: :boolean, was setting { banner: "" } 2017-01-06 17:16:57 -06:00
test Fix method redefined warning 2017-01-06 17:14:56 -06:00
.gitignore adds mac files to .gitignore (#1728) 2016-05-17 13:51:26 -04:00
.rubocop.yml re: RuboCop - replace rocket style hashes 2016-06-21 22:08:27 +01:00
.simplecov Update SimpleCov; remove compatibility patch 2016-02-09 20:59:31 -06:00
.travis.yml Run tests by Ruby 2.2.6 and 2.3.3 2016-12-04 00:20:24 +09:00
active_model_serializers.gemspec Update jsonapi runtime dependency to 0.1.1.beta6 2016-12-16 18:00:49 +01:00
appveyor.yml Fix appveyor setting for JRuby 2016-04-25 09:24:51 +09:00
CHANGELOG.md Bump to v0.10.4 2017-01-06 16:23:54 -06:00
CODE_OF_CONDUCT.md add a code of conduct 2016-08-31 23:03:28 +02:00
CONTRIBUTING.md Bump to v0.10.0 2016-05-17 12:49:37 -06:00
Gemfile added active record benchmark (#1919) 2016-09-24 16:39:29 -04:00
MIT-LICENSE Improvements from Rails plugin template 2016-04-01 05:39:03 -05:00
Rakefile re: RuboCop - Suppress of handling LoadError for optional dependencies 2016-06-20 22:15:58 +01:00
README.md Bump to v0.10.4 2017-01-06 16:23:54 -06:00

ActiveModelSerializers

Build Status Build Status Build status
Code Quality Code Quality codebeat Test Coverage
Issue Stats Pulse

About

ActiveModelSerializers brings convention over configuration to your JSON generation.

ActiveModelSerializers works through two components: serializers and adapters.

Serializers describe which attributes and relationships should be serialized.

Adapters describe how attributes and relationships should be serialized.

SerializableResource co-ordinates the resource, Adapter and Serializer to produce the resource serialization. The serialization has the #as_json, #to_json and #serializable_hash methods used by the Rails JSON Renderer. (SerializableResource actually delegates these methods to the adapter.)

By default ActiveModelSerializers will use the Attributes Adapter (no JSON root). But we strongly advise you to use JsonApi Adapter, which follows 1.0 of the format specified in jsonapi.org/format. Check how to change the adapter in the sections below.

0.10.x is not backward compatible with 0.9.x nor 0.8.x.

0.10.x is based on the 0.8.0 code, but with a more flexible architecture. We'd love your help. Learn how you can help here.

It is generally safe and recommended to use the master branch.

Installation

Add this line to your application's Gemfile:

gem 'active_model_serializers', '~> 0.10.0'

And then execute:

$ bundle

Getting Started

See Getting Started for the nuts and bolts.

More information is available in the Guides and High-level behavior.

Getting Help

If you find a bug, please report an Issue and see our contributing guide.

If you have a question, please post to Stack Overflow.

If you'd like to chat, we have a community slack.

Thanks!

Documentation

If you're reading this at https://github.com/rails-api/active_model_serializers you are reading documentation for our master, which may include features that have not been released yet. Please see below for the documentation relevant to you.

High-level behavior

Choose an adapter from adapters:

ActiveModelSerializers.config.adapter = :json_api # Default: `:attributes`

Given a serializable model:

# either
class SomeResource < ActiveRecord::Base
  # columns: title, body
end
# or
class SomeResource < ActiveModelSerializers::Model
  attributes :title, :body
end

And initialized as:

resource = SomeResource.new(title: 'ActiveModelSerializers', body: 'Convention over configuration')

Given a serializer for the serializable model:

class SomeSerializer < ActiveModel::Serializer
  attribute :title, key: :name
  attributes :body
end

The model can be serialized as:

options = {}
serialization = ActiveModelSerializers::SerializableResource.new(resource, options)
serialization.to_json
serialization.as_json

SerializableResource delegates to the adapter, which it builds as:

adapter_options = {}
adapter = ActiveModelSerializers::Adapter.create(serializer, adapter_options)
adapter.to_json
adapter.as_json
adapter.serializable_hash

The adapter formats the serializer's attributes and associations (a.k.a. includes):

serializer_options = {}
serializer = SomeSerializer.new(resource, serializer_options)
serializer.attributes
serializer.associations

Architecture

This section focuses on architecture the 0.10.x version of ActiveModelSerializers. If you are interested in the architecture of the 0.8 or 0.9 versions, please refer to the 0.8 README or 0.9 README.

The original design is also available here.

ActiveModel::Serializer

An ActiveModel::Serializer wraps a serializable resource and exposes an attributes method, among a few others. It allows you to specify which attributes and associations should be represented in the serializatation of the resource. It requires an adapter to transform its attributes into a JSON document; it cannot be serialized itself. It may be useful to think of it as a presenter.

ActiveModel::CollectionSerializer

The ActiveModel::CollectionSerializer represents a collection of resources as serializers and, if there is no serializer, primitives.

ActiveModelSerializers::Adapter::Base

The ActiveModelSerializeres::Adapter::Base describes the structure of the JSON document generated from a serializer. For example, the Attributes example represents each serializer as its unmodified attributes. The JsonApi adapter represents the serializer as a JSON API document.

ActiveModelSerializers::SerializableResource

The ActiveModelSerializers::SerializableResource acts to coordinate the serializer(s) and adapter to an object that responds to to_json, and as_json. It is used in the controller to encapsulate the serialization resource when rendered. However, it can also be used on its own to serialize a resource outside of a controller, as well.

Primitive handling

Definitions: A primitive is usually a String or Array. There is no serializer defined for them; they will be serialized when the resource is converted to JSON (as_json or to_json). (The below also applies for any object with no serializer.)

  • ActiveModelSerializers doesn't handle primitives passed to render json: at all.

Internally, if no serializer can be found in the controller, the resource is not decorated by ActiveModelSerializers.

  • However, when a primitive value is an attribute or in a collection, it is not modified.

When serializing a collection and the collection serializer (CollectionSerializer) cannot identify a serializer for a resource in its collection, it throws :no_serializer. For example, when caught by Reflection#build_association, and the association value is set directly:

reflection_options[:virtual_value] = association_value.try(:as_json) || association_value

(which is called by the adapter as serializer.associations(*).)

How options are parsed

High-level overview:

  • For a collection
    • :serializer specifies the collection serializer and
    • :each_serializer specifies the serializer for each resource in the collection.
  • For a single resource, the :serializer option is the resource serializer.
  • Options are partitioned in serializer options and adapter options. Keys for adapter options are specified by ADAPTER_OPTION_KEYS. The remaining options are serializer options.

Details:

  1. ActionController::Serialization
  2. serializable_resource = ActiveModelSerializers::SerializableResource.new(resource, options) 1. options are partitioned into adapter_opts and everything else (serializer_opts). The adapter_opts keys are defined in ActiveModelSerializers::SerializableResource::ADAPTER_OPTION_KEYS.
  3. ActiveModelSerializers::SerializableResource
  4. if serializable_resource.serializer? (there is a serializer for the resource, and an adapter is used.) - Where serializer? is use_adapter? && !!(serializer)
    • Where use_adapter?: 'True when no explicit adapter given, or explicit value is truthy (non-nil); False when explicit adapter is falsy (nil or false)'
    • Where serializer:
      1. from explicit :serializer option, else
      2. implicitly from resource ActiveModel::Serializer.serializer_for(resource)
  5. A side-effect of checking serializer is:
    • The :serializer option is removed from the serializer_opts hash
    • If the :each_serializer option is present, it is removed from the serializer_opts hash and set as the :serializer option
  6. The serializer and adapter are created as 1. serializer_instance = serializer.new(resource, serializer_opts) 2. adapter_instance = ActiveModel::Serializer::Adapter.create(serializer_instance, adapter_opts)
  7. ActiveModel::Serializer::CollectionSerializer#new
  8. If the serializer_instance was a CollectionSerializer and the :serializer serializer_opts is present, then that serializer is passed into each resource.
  9. ActiveModel::Serializer#attributes is used by the adapter to get the attributes for resource as defined by the serializer.

(In Rails, the options are also passed to the as_json(options) or to_json(options) methods on the resource serialization by the Rails JSON renderer. They are, therefore, important to know about, but not part of ActiveModelSerializers.)

What does a 'serializable resource' look like?

  • An ActiveRecord::Base object.
  • Any Ruby object that passes the Lint code.

ActiveModelSerializers provides a ActiveModelSerializers::Model, which is a simple serializable PORO (Plain-Old Ruby Object).

ActiveModelSerializers::Model may be used either as a reference implementation, or in production code.

class MyModel < ActiveModelSerializers::Model
  attributes :id, :name, :level
end

The default serializer for MyModel would be MyModelSerializer whether MyModel is an ActiveRecord::Base object or not.

Outside of the controller the rules are exactly the same as for records. For example:

render json: MyModel.new(level: 'awesome'), adapter: :json

would be serialized the same as

ActiveModelSerializers::SerializableResource.new(MyModel.new(level: 'awesome'), adapter: :json).as_json

Semantic Versioning

This project adheres to semver

Contributing

See CONTRIBUTING.md