Skip to content

Commit

Permalink
Merge pull request #1253 from rails-api/architecture
Browse files Browse the repository at this point in the history
Describe AMS architecture in the big picture
  • Loading branch information
bf4 committed Oct 22, 2015
2 parents 8e1245a + d3a8006 commit 9b0c865
Showing 1 changed file with 118 additions and 0 deletions.
118 changes: 118 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# ARCHITECTURE

An **`ActiveModel::Serializer`** wraps a [serializable resource](https://github.com/rails/rails/blob/4-2-stable/activemodel/lib/active_model/serialization.rb)
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](http://blog.steveklabnik.com/posts/2011-09-09-better-ruby-presenters).

The **`ActiveModel::ArraySerializer`** represent a collection of resources as serializers
and, if there is no serializer, primitives.

The **`ActiveModel::Adapter`** 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](jsonapi.org/) document.

The **`ActiveModel::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.

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

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

If the collection serializer (ArraySerializer) cannot
identify a serializer for a resource in its collection, it raises [`NoSerializerError`](https://github.com/rails-api/active_model_serializers/issues/1191#issuecomment-142327128)
which is rescued in `AcitveModel::Serializer::Reflection#build_association` which sets
the association value directly:

```ruby
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_OPTIONS`](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model/serializable_resource.rb#L4).
The remaining options are serializer options.

Details:

1. **ActionController::Serialization**
1. `serializable_resource = ActiveModel::SerializableResource.new(resource, options)`
1. `options` are partitioned into `adapter_opts` and everything else (`serializer_opts`).
The adapter options keys for the are defined by `ADAPTER_OPTIONS`.
1. **ActiveModel::SerializableResource**
1. `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)`
1. 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
1. 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)`
1. **ActiveModel::Serializer::ArraySerializer#new**
1. If the `serializer_instance` was a `ArraySerializer` and the `:serializer` serializer_opts
is present, then [that serializer is passed into each resource](https://github.com/rails-api/active_model_serializers/blob/a54d237e2828fe6bab1ea5dfe6360d4ecc8214cd/lib/active_model/serializer/array_serializer.rb#L14-L16).
1. **ActiveModel::Serializer#attributes** is used by the adapter to get the attributes for
resource as defined by the serializer.

## What does a 'serializable resource' look like?

- An `ActiveRecord::Base` object.
- Any Ruby object at passes or otherwise passes the
[Lint](http://www.rubydoc.info/github/rails-api/active_model_serializers/ActiveModel/Serializer/Lint/Tests)
[code](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model/serializer/lint.rb).

ActiveModelSerializers provides a
`[ActiveModelSerializers::Model](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model_serializers/model.rb)`,
which is a simple serializable PORO (Plain-Old Ruby Object).

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

```ruby
class MyModel < ActiveModelSerializers::Model
attr_accessor :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:

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

would be serialized the same as

```ruby
ActiveModel::SerializableResource.new(MyModel.new(level: 'awesome'), adapter: :json).as_json
```

0 comments on commit 9b0c865

Please sign in to comment.