-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1253 from rails-api/architecture
Describe AMS architecture in the big picture
- Loading branch information
Showing
1 changed file
with
118 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |