|
| 1 | +# ARCHITECTURE |
| 2 | + |
| 3 | +An **`ActiveModel::Serializer`** wraps a [serializable resource](https://github.com/rails/rails/blob/4-2-stable/activemodel/lib/active_model/serialization.rb) |
| 4 | +and exposes an `attributes` method, among a few others. |
| 5 | +It allows you to specify which attributes and associations should represent the serialization of the resource. |
| 6 | +It requires an adapter to transform its attributes into a JSON document; it cannot be serialized itself. |
| 7 | +It may be useful to think of it as a |
| 8 | +[presenter](http://blog.steveklabnik.com/posts/2011-09-09-better-ruby-presenters). |
| 9 | + |
| 10 | +The **`ActiveModel::ArraySerializer`** represent a collection of resources as serializers |
| 11 | +and, if there is no serializer, primitives. |
| 12 | + |
| 13 | +The **`ActiveModel::Adapter`** describes the structure of the JSON document generated from a |
| 14 | +serializer. For example, the `Attributes` example represents each serializer as its |
| 15 | +unmodified attributes. The `JsonApi` adatper represents the serializer as a [JSON |
| 16 | +API](jsonapi.org/) document. |
| 17 | + |
| 18 | +The **`ActiveModel::SerializableResource`** acts to coordinate the serializer(s) and adapter |
| 19 | +to an object that responds to `to_json`, and `as_json`. It is used in the controller to |
| 20 | +encapsulate the serialization resource when rendered. Thus, it can be used on its own |
| 21 | +to serialize a resource outside of a controller, as well. |
| 22 | + |
| 23 | +## Primitive handling |
| 24 | + |
| 25 | +Definitions: A primitive is usually a String or Array. There is no serializer |
| 26 | +defined for them; they will be serialized when the resource is converted to JSON (`as_json` or |
| 27 | +`to_json`). (The below also applies for any object with no serializer.) |
| 28 | + |
| 29 | +ActiveModelSerializers doesn't handle primitives passed to `render json:` at all. |
| 30 | + |
| 31 | +However, when a primitive value is an attribute or in a collection, |
| 32 | +it is is not modified. |
| 33 | + |
| 34 | +Internally, if no serializer can be found in the controller, the resource is not decorated by |
| 35 | +ActiveModelSerializers. |
| 36 | + |
| 37 | +If the collection serializer (ArraySerializer) cannot |
| 38 | +identify a serializer for a resource in its collection, it raises [`NoSerializerError`](https://github.com/rails-api/active_model_serializers/issues/1191#issuecomment-142327128) |
| 39 | +which is rescued in `AcitveModel::Serializer::Reflection#build_association` which sets |
| 40 | +the association value directly: |
| 41 | + |
| 42 | +```ruby |
| 43 | +reflection_options[:virtual_value] = association_value.try(:as_json) || association_value |
| 44 | +``` |
| 45 | + |
| 46 | +(which is called by the adapter as `serializer.associations(*)`.) |
| 47 | + |
| 48 | +## How options are parsed |
| 49 | + |
| 50 | +High-level overview: |
| 51 | + |
| 52 | +- For a collection |
| 53 | + - the collection serializer is the `:serializer` option and |
| 54 | + - `:each_serializer` is used as the serializer for each resource in the collection. |
| 55 | +- For a single resource, the `:serializer` option is the resource serializer. |
| 56 | +- Options are partitioned in serializer options and adapter options. Keys for adapter options are specified by |
| 57 | + [`ADAPTER_OPTIONS`](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model/serializable_resource.rb#L4). |
| 58 | + The remaining options are serializer options. |
| 59 | + |
| 60 | +Details: |
| 61 | + |
| 62 | +1. **ActionController::Serialization** |
| 63 | + 1. `serializable_resource = ActiveModel::SerializableResource.new(resource, options)` |
| 64 | + 1. `options` are partitioned into `adapter_opts` and everything else (`serializer_opts`). |
| 65 | + The adapter options keys for the are defined by `ADAPTER_OPTIONS`. |
| 66 | +1. **ActiveModel::SerializableResource** |
| 67 | + 1. `if serializable_resource.serializer?` (there is a serializer for the resource, and an adapter is used.) |
| 68 | + - Where `serializer?` is `use_adapter? && !!(serializer)` |
| 69 | + - Where `use_adapter?`: 'True when no explicit adapter given, or explicit value is truthy (non-nil); |
| 70 | + False when explicit adapter is falsy (nil or false)' |
| 71 | + - Where `serializer`: |
| 72 | + 1. from explicit `:serializer` option, else |
| 73 | + 2. implicitly from resource `ActiveModel::Serializer.serializer_for(resource)` |
| 74 | + 1. A side-effect of checking `serializer` is: |
| 75 | + - The `:serializer` option is removed from the serializer_opts hash |
| 76 | + - If the `:each_serializer` option is present, it is removed from the serializer_opts hash and set as the `:serializer` option |
| 77 | + 1. The serializer and adapter are created as |
| 78 | + 1. `serializer_instance = serializer.new(resource, serializer_opts)` |
| 79 | + 2. `adapter_instance = ActiveModel::Serializer::Adapter.create(serializer_instance, adapter_opts)` |
| 80 | +1. **ActiveModel::Serializer::ArraySerializer#new** |
| 81 | + 1. If the `serializer_instance` was a `ArraySerializer` and the `:serializer` serializer_opts |
| 82 | + 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). |
| 83 | +1. **ActiveModel::Serializer#attributes** is used by the adapter to get the attributes for |
| 84 | + resource as defined by the serializer. |
| 85 | + |
| 86 | +## What does a 'serializable resource' look like? |
| 87 | + |
| 88 | +- An `ActiveRecord::Base` object. |
| 89 | +- Any Ruby object at passes or otherwise passes the |
| 90 | + [Lint](http://www.rubydoc.info/github/rails-api/active_model_serializers/ActiveModel/Serializer/Lint/Tests) |
| 91 | + [code](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model/serializer/lint.rb). |
| 92 | + |
| 93 | +ActiveModelSerializers provides a |
| 94 | +`[ActiveModelSerializers::Model](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model_serializers/model.rb)`, |
| 95 | +which is a simple serializable PORO (plain-old Ruby object). |
| 96 | + |
| 97 | +ActiveModelSerializers::Model may be used either as a template, or in production code. |
| 98 | + |
| 99 | +```ruby |
| 100 | +class MyModel < ActiveModelSerializers::Model |
| 101 | + attr_accessor :id, :name, :level |
| 102 | +end |
| 103 | +``` |
| 104 | + |
| 105 | +The default serializer for `MyModel` would be `MyModelSerializer` whether MyModel is an |
| 106 | +ActiveRecord::Base object or not. |
| 107 | + |
| 108 | +Outside of the controller the rules are **exactly** the same as for records. For example: |
| 109 | + |
| 110 | +```ruby |
| 111 | +render json: MyModel.new(level: 'awesome'), adapter: :json |
| 112 | +``` |
| 113 | + |
| 114 | +would be serialized the same as |
| 115 | + |
| 116 | +```ruby |
| 117 | +ActiveModel::SerializableResource.new(MyModel.new(level: 'awesome'), adapter: :json).as_json |
| 118 | +``` |
0 commit comments