README.md
[![Gem Version](https://badge.fury.io/rb/serega.svg)](https://badge.fury.io/rb/serega)
[![GitHub Actions](https://github.com/aglushkov/serega/actions/workflows/main.yml/badge.svg?event=push)](https://github.com/aglushkov/serega/actions/workflows/main.yml)
[![Test Coverage](https://api.codeclimate.com/v1/badges/f10c0659e16e25e49faa/test_coverage)](https://codeclimate.com/github/aglushkov/serega/test_coverage)
[![Maintainability](https://api.codeclimate.com/v1/badges/f10c0659e16e25e49faa/maintainability)](https://codeclimate.com/github/aglushkov/serega/maintainability)
# Serega Ruby Serializer
The Serega Ruby Serializer provides easy and powerful DSL to describe your
objects and serialize them to Hash or JSON.
---
📌 Serega does not depend on any gem and works with any framework
---
It has some great features:
- Manually [select serialized fields](#selecting-fields)
- Secure from malicious queries with [depth_limit][depth_limit] plugin
- Solutions for N+1 problem (via [batch][batch], [preloads][preloads] or
[activerecord_preloads][activerecord_preloads] plugins)
- Built-in object presenter ([presenter][presenter] plugin)
- Adding custom metadata (via [metadata][metadata] or
[context_metadata][context_metadata] plugins)
- Value formatters ([formatters][formatters] plugin) helps to transform
time, date, money, percentage, and any other values in the same way keeping
the code dry
- Conditional attributes - ([if][if] plugin)
- Auto camelCase keys - [camel_case][camel_case] plugin
## Installation
`bundle add serega`
### Define serializers
Most apps should define **base serializer** with common plugins and settings to
not repeat them in each serializer.
Serializers will inherit everything (plugins, config, attributes) from their
superclasses.
```ruby
class AppSerializer < Serega
# plugin :one
# plugin :two
# config.one = :one
# config.two = :two
end
class UserSerializer < AppSerializer
# attribute :one
# attribute :two
end
class CommentSerializer < AppSerializer
# attribute :one
# attribute :two
end
```
### Adding attributes
```ruby
class UserSerializer < Serega
# Regular attribute
attribute :first_name
# Option :method specifies the method that must be called on the serialized object
attribute :first_name, method: :old_first_name
# Block is used to define attribute value
attribute(:first_name) { |user| user.profile&.first_name }
# Option :value can be used with a Proc or callable object to define attribute
# value
attribute :first_name, value: UserProfile.new # must have #call method
attribute :first_name, value: proc { |user| user.profile&.first_name }
# Option :delegate can be used to define attribute value.
# Sub-option :allow_nil by default is false
attribute :first_name, delegate: { to: :profile, allow_nil: true }
# Option :delegate can be used with :method sub-option, so method chain here
# is user.profile.fname
attribute :first_name, delegate: { to: :profile, method: :fname }
# Option :default can be used to replace possible nil values.
attribute :first_name, default: ''
attribute :is_active, default: false
attribute :comments_count, default: 0
# Option :const specifies attribute with a specific constant value
attribute(:type, const: 'user')
# Option :hide specifies attributes that should not be serialized by default
attribute :tags, hide: true
# Option :serializer specifies nested serializer for attribute
# We can define the `:serializer` value as a Class, String, or Proc.
# Use String or Proc if you have cross-references in serializers.
attribute :posts, serializer: PostSerializer
attribute :posts, serializer: "PostSerializer"
attribute :posts, serializer: -> { PostSerializer }
# Option `:many` specifies a has_many relationship. It is optional.
# If not specified, it is defined during serialization by checking `object.is_a?(Enumerable)`
# Also the `:many` changes the default value from `nil` to `[]`.
attribute :posts, serializer: PostSerializer, many: true
# Option `:preload` can be specified when enabled `:preloads` plugin
# It allows to specify associations to preload to attribute value
attribute(:email, preload: :emails) { |user| user.emails.find(&:verified?) }
# Options `:if, :unless, :if_value and :unless_value` can be specified
# when `:if` plugin is enabled. They hide the attribute key and value from the
# response.
# See more usage examples in the `:if` plugin section.
attribute :email, if: proc { |user, ctx| user == ctx[:current_user] }
attribute :email, if_value: :present?
# Option `:format` can be specified when enabled `:formatters` plugin
# It changes the attribute value
attribute :created_at, format: :iso_time
attribute :updated_at, format: :iso_time
# Option `:format` also can be used as Proc
attribute :created_at, format: proc { |time| time.strftime("%Y-%m-%d")}
end
```
---
⚠️ Attribute names are checked to include only "a-z", "A-Z", "0-9", "\_", "-",
"~" characters.
We allow ONLY these characters as we want to be able to use attribute names in
URLs without escaping.
The check can be turned off:
```ruby
# Disable globally
Serega.config.check_attribute_name = false
# Disable for specific serializer
class SomeSerializer < Serega
config.check_attribute_name = false
end
```
### Serializing
We can serialize objects using class methods `.to_h`, `.to_json`, `.as_json` and
same instance methods `#to_h`, `#to_json`, `#as_json`.
The `to_h` method is also aliased as `call`.
```ruby
user = OpenStruct.new(username: 'serega')
class UserSerializer < Serega
attribute :username
end
UserSerializer.to_h(user) # => {username: "serega"}
UserSerializer.to_h([user]) # => [{username: "serega"}]
UserSerializer.to_json(user) # => '{"username":"serega"}'
UserSerializer.to_json([user]) # => '[{"username":"serega"}]'
UserSerializer.as_json(user) # => {"username":"serega"}
UserSerializer.as_json([user]) # => [{"username":"serega"}]
```
If serialized fields are constant, then it's a good idea to initiate the
serializer and reuse it.
It will be a bit faster (the serialization plan will be prepared only once).
```ruby
# Example with all fields
serializer = UserSerializer.new
serializer.to_h(user1)
serializer.to_h(user2)
# Example with custom fields
serializer = UserSerializer.new(only: [:username, :avatar])
serializer.to_h(user1)
serializer.to_h(user2)
```
---
⚠️ When you serialize the `Struct` object, specify manually `many: false`. As Struct
is Enumerable and we check `object.is_a?(Enumerable)` to detect if we should
return array.
```ruby
UserSerializer.to_h(user_struct, many: false)
```
### Selecting Fields
By default, all attributes are serialized (except marked as `hide: true`).
We can provide **modifiers** to select serialized attributes:
- *only* - lists specific attributes to serialize;
- *except* - lists attributes to not serialize;
- *with* - lists attributes to serialize additionally (By default all attributes
are exposed and will be serialized, but some attributes can be hidden when
they are defined with the `hide: true` option, more on this below. `with`
modifier can be used to expose such attributes).
Modifiers can be provided as Hash, Array, String, Symbol, or their combinations.
With plugin [string_modifiers][string_modifiers] we can provide modifiers as
single `String` with attributes split by comma `,` and nested values inside
brackets `()`, like: `username,enemies(username,email)`. This can be very useful
to accept the list of fields in **GET** requests.
When a non-existing attribute is provided, the `Serega::AttributeNotExist` error
will be raised. This error can be muted with the `check_initiate_params: false`
option.
```ruby
class UserSerializer < Serega
plugin :string_modifiers # to send all modifiers in one string
attribute :username
attribute :first_name
attribute :last_name
attribute :email, hide: true
attribute :enemies, serializer: UserSerializer, hide: true
end
joker = OpenStruct.new(
username: 'The Joker',
first_name: 'jack',
last_name: 'Oswald White',
email: 'joker@mail.com',
enemies: []
)
bruce = OpenStruct.new(
username: 'Batman',
first_name: 'Bruce',
last_name: 'Wayne',
email: 'bruce@wayneenterprises.com',
enemies: []
)
joker.enemies << bruce
bruce.enemies << joker
# Default
UserSerializer.to_h(bruce)
# => {:username=>"Batman", :first_name=>"Bruce", :last_name=>"Wayne"}
# With `:only` modifier
fields = [:username, { enemies: [:username, :email] }]
fields_as_string = 'username,enemies(username,email)'
UserSerializer.to_h(bruce, only: fields)
UserSerializer.new(only: fields).to_h(bruce)
UserSerializer.new(only: fields_as_string).to_h(bruce)
# =>
# {
# :username=>"Batman",
# :enemies=>[{:username=>"The Joker", :email=>"joker@mail.com"}]
# }
# With `:except` modifier
fields = %i[first_name last_name]
fields_as_string = 'first_name,last_name'
UserSerializer.new(except: fields).to_h(bruce)
UserSerializer.to_h(bruce, except: fields)
UserSerializer.to_h(bruce, except: fields_as_string)
# => {:username=>"Batman"}
# With `:with` modifier
fields = %i[email enemies]
fields_as_string = 'email,enemies'
UserSerializer.new(with: fields).to_h(bruce)
UserSerializer.to_h(bruce, with: fields)
UserSerializer.to_h(bruce, with: fields_as_string)
# =>
# {
# :username=>"Batman",
# :first_name=>"Bruce",
# :last_name=>"Wayne",
# :email=>"bruce@wayneenterprises.com",
# :enemies=>[
# {:username=>"The Joker", :first_name=>"jack", :last_name=>"Oswald White"}
# ]
# }
# With no existing attribute
fields = %i[first_name enemy]
fields_as_string = 'first_name,enemy'
UserSerializer.new(only: fields).to_h(bruce)
UserSerializer.to_h(bruce, only: fields)
UserSerializer.to_h(bruce, only: fields_as_string)
# => raises Serega::AttributeNotExist
# With no existing attribute and disabled validation
fields = %i[first_name enemy]
fields_as_string = 'first_name,enemy'
UserSerializer.new(only: fields, check_initiate_params: false).to_h(bruce)
UserSerializer.to_h(bruce, only: fields, check_initiate_params: false)
UserSerializer.to_h(bruce, only: fields_as_string, check_initiate_params: false)
# => {:first_name=>"Bruce"}
```
### Using Context
Sometimes it can be required to use the context during serialization, like
current_user or any.
```ruby
class UserSerializer < Serega
attribute(:email) do |user, ctx|
user.email if ctx[:current_user] == user
end
end
user = OpenStruct.new(email: 'email@example.com')
UserSerializer.(user, context: {current_user: user})
# => {:email=>"email@example.com"}
UserSerializer.new.to_h(user, context: {current_user: user}) # same
# => {:email=>"email@example.com"}
```
## Configuration
Here are the default options. Other options can be added with plugins.
```ruby
class AppSerializer < Serega
# Configure adapter to serialize to JSON.
# It is `JSON.dump` by default. But if the Oj gem is loaded, then the default
# is changed to `Oj.dump(data, mode: :compat)`
config.to_json = ->(data) { Oj.dump(data, mode: :compat) }
# Configure adapter to de-serialize JSON.
# De-serialization is used only for the `#as_json` method.
# It is `JSON.parse` by default.
# When the Oj gem is loaded, then the default is `Oj.load(data)`
config.from_json = ->(data) { Oj.load(data) }
# Disable/enable validation of modifiers (`:with, :except, :only`)
# By default, this validation is enabled.
# After disabling, all requested incorrect attributes will be skipped.
config.check_initiate_params = false # default is true, enabled
# Stores in memory prepared `plans` - list of serialized attributes.
# Next time serialization happens with the same modifiers (`only, except, with`),
# we will reuse already prepared `plans`.
# This defines storage size (count of stored `plans` with different modifiers).
config.max_cached_plans_per_serializer_count = 50 # default is 0, disabled
end
```
## Plugins
### Plugin :preloads
Allows to define `:preloads` to attributes and then allows to merge preloads
from serialized attributes and return single associations hash.
Plugin accepts options:
- `auto_preload_attributes_with_delegate` - default `false`
- `auto_preload_attributes_with_serializer` - default `false`
- `auto_hide_attributes_with_preload` - default `false`
These options are extremely useful if you want to forget about finding preloads
manually.
Preloads can be disabled with the `preload: false` attribute option.
Automatically added preloads can be overwritten with the manually specified
`preload: :xxx` option.
For some examples, **please read the comments in the code below**
```ruby
class AppSerializer < Serega
plugin :preloads,
auto_preload_attributes_with_delegate: true,
auto_preload_attributes_with_serializer: true,
auto_hide_attributes_with_preload: true
end
class UserSerializer < AppSerializer
# No preloads
attribute :username
# `preload: :user_stats` added manually
attribute :followers_count, preload: :user_stats,
value: proc { |user| user.user_stats.followers_count }
# `preload: :user_stats` added automatically, as
# `auto_preload_attributes_with_delegate` option is true
attribute :comments_count, delegate: { to: :user_stats }
# `preload: :albums` added automatically as
# `auto_preload_attributes_with_serializer` option is true
attribute :albums, serializer: 'AlbumSerializer'
end
class AlbumSerializer < AppSerializer
attribute :images_count, delegate: { to: :album_stats }
end
# By default, preloads are empty, as we specify `auto_hide_attributes_with_preload`
# so attributes with preloads will be skipped and nothing will be preloaded
UserSerializer.new.preloads
# => {}
UserSerializer.new(with: :followers_count).preloads
# => {:user_stats=>{}}
UserSerializer.new(with: %i[followers_count comments_count]).preloads
# => {:user_stats=>{}}
UserSerializer.new(
with: [:followers_count, :comments_count, { albums: :images_count }]
).preloads
# => {:user_stats=>{}, :albums=>{:album_stats=>{}}}
```
---
#### SPECIFIC CASE #1: Serializing the same object in association
For example, you show your current user as "user" and use the same user object
to serialize "user_stats". `UserStatSerializer` relies on user fields and any
other user associations. You should specify `preload: nil` to preload
`UserStatSerializer` nested associations to the "user" object.
```ruby
class AppSerializer < Serega
plugin :preloads,
auto_preload_attributes_with_delegate: true,
auto_preload_attributes_with_serializer: true,
auto_hide_attributes_with_preload: true
end
class UserSerializer < AppSerializer
attribute :username
attribute :user_stats,
serializer: 'UserStatSerializer',
value: proc { |user| user },
preload: nil
end
```
#### SPECIFIC CASE #2: Serializing multiple associations as a single relation
For example, "user" has two relations - "new_profile" and "old_profile". Also
profiles have the "avatar" association. And you decided to serialize profiles in
one array. You can specify `preload_path: [[:new_profile], [:old_profile]]` to
achieve this:
```ruby
class AppSerializer < Serega
plugin :preloads,
auto_preload_attributes_with_delegate: true,
auto_preload_attributes_with_serializer: true
end
class UserSerializer < AppSerializer
attribute :username
attribute :profiles,
serializer: 'ProfileSerializer',
value: proc { |user| [user.new_profile, user.old_profile] },
preload: [:new_profile, :old_profile],
preload_path: [[:new_profile], [:old_profile]] # <--- like here
end
class ProfileSerializer < AppSerializer
attribute :avatar, serializer: 'AvatarSerializer'
end
class AvatarSerializer < AppSerializer
end
UserSerializer.new.preloads
# => {:new_profile=>{:avatar=>{}}, :old_profile=>{:avatar=>{}}}
```
#### SPECIFIC CASE #3: Preload association through another association
```ruby
attribute :image,
preload: { attachment: :blob }, # <--------- like this one
value: proc { |record| record.attachment },
serializer: ImageSerializer,
preload_path: [:attachment] # or preload_path: [:attachment, :blob]
```
In this case, we don't know if preloads defined in ImageSerializer, should be
preloaded to `attachment` or `blob`, so please specify `preload_path` manually.
You can specify `preload_path: nil` if you are sure that there are no preloads
inside ImageSerializer.
---
📌 Plugin `:preloads` only allows to group preloads together in single Hash, but
they should be preloaded manually.
There are only [activerecord_preloads][activerecord_preloads] plugin that can
be used to preload these associations automatically.
### Plugin :activerecord_preloads
(depends on [preloads][preloads] plugin, that must be loaded first)
Automatically preloads associations to serialized objects.
It takes all defined preloads from serialized attributes (including attributes
from serialized relations), merges them into a single associations hash, and then
uses ActiveRecord::Associations::Preloader to preload associations to objects.
```ruby
class AppSerializer < Serega
plugin :preloads,
auto_preload_attributes_with_delegate: true,
auto_preload_attributes_with_serializer: true,
auto_hide_attributes_with_preload: false
plugin :activerecord_preloads
end
class UserSerializer < AppSerializer
attribute :username
attribute :comments_count, delegate: { to: :user_stats }
attribute :albums, serializer: AlbumSerializer
end
class AlbumSerializer < AppSerializer
attribute :title
attribute :downloads_count, preload: :downloads,
value: proc { |album| album.downloads.count }
end
UserSerializer.to_h(user)
# => preloads {users_stats: {}, albums: { downloads: {} }}
```
### Plugin :batch
Helps to omit N+1.
User must specify how attribute values are loaded -
`attribute :foo, batch: {loader: SomeLoader, id_method: :id}`.
The result must be returned as Hash, where each key is one of the provided IDs.
```ruby
class AppSerializer
plugin :batch
end
class UserSerializer < AppSerializer
attribute :comments_count,
batch: { loader: SomeLoader, id_method: :id }
attribute :company,
batch: { loader: SomeLoader, id_method: :id },
serializer: CompanySerializer
end
```
#### Option :loader
Loaders can be defined as a Proc, a callable value, or a named Symbol
Named loaders should be predefined with
`config.batch.define(:loader_name) { |ids| ... })`
The loader can accept 1 to 3 arguments:
1. List of IDs (each ID will be found by using the `:id_method` option)
1. Context
1. PlanPoint - a special object containing information about current
attribute and all children and parent attributes. It can be used to preload
required associations to batch values.
See [example](examples/batch_loader.rb) how
to find required preloads when using the `:preloads` plugin.
```ruby
class AppSerializer < Serega
plugin :batch, id_method: :id
end
class UserSerializer < Serega
# Define loader as a callable object
attribute :comments_count,
batch: { loader: CountLoader }
# Define loader as a Proc
attribute :comments_count,
batch: { loader: proc { |ids| CountLoader.call(ids) } }
# Define loader as a Symbol
config.batch.define(:comments_count_loader) { |ids| CountLoader.call(ids }
attribute :comments_count, batch: { loader: :comments_count_loader }
end
class CountLoader
def self.call(user_ids)
Comment.where(user_id: user_ids).group(:user_id).count
end
end
```
#### Option :id_method
The `:batch` plugin can be added with the global `:id_method` option. It can be
a Symbol, Proc or any callable value that can accept the current object and
context.
```ruby
class SomeSerializer
plugin :batch, id_method: :id
end
class UserSerializer < AppSerializer
attribute :comments_count,
batch: { loader: CommentsCountBatchLoader } # no :id_method here anymore
attribute :company,
batch: { loader: UserCompanyBatchLoader }, # no :id_method here anymore
serializer: CompanySerializer
end
```
However, the global `id_method` option can be overwritten via
`config.batch.id_method=` method or in specific attributes with the `id_method`
option.
```ruby
class SomeSerializer
plugin :batch, id_method: :id # global id_method is `:id`
end
class UserSerializer < AppSerializer
# :user_id will be used as default `id_method` for all batch attributes
config.batch.id_method = :user_id
# id_method is :user_id
attribute :comments_count,
batch: { loader: CommentsCountBatchLoader }
# id_method is :user_id
attribute :company,
batch: { loader: UserCompanyBatchLoader }, serializer: CompanySerializer
# id_method is :uuid
attribute :points_amount,
batch: { loader: PointsBatchLoader, id_method: :uuid }
end
```
#### Default value
The default value for attributes without found value can be specified via
`:default` option. By default, attributes without found value will be
serialized as a `nil` value. Attributes marked as `many: true` will be
serialized as empty array `[]` values.
```ruby
class UserSerializer < AppSerializer
# Missing values become empty arrays, as the `many: true` option is specified
attribute :companies,
batch: {loader: proc {}},
serializer: CompanySerializer,
many: true
# Missing values become `0` as specified directly
attribute :points_amount, batch: { loader: proc {} }, default: 0
end
```
Batch attributes can be marked as hidden by default if the plugin is enabled
with the `auto_hide` option. The `auto_hide` option can be changed with
the `config.batch.auto_hide=` method.
Look at [select serialized fields](#selecting-fields) for more information
about hiding/showing attributes.
```ruby
class AppSerializer
plugin :batch, auto_hide: true
end
class UserSerializer < AppSerializer
config.batch.auto_hide = false
end
```
---
⚠️ ATTENTION: The `:batch` plugin must be added to all serializers that have
`:batch` attributes inside nested serializers. For example, when you serialize
the `User -> Album -> Song` and the Song has a `batch` attribute, then
the `:batch` plugin must be added to the User serializer.
The best way would be to create one parent `AppSerializer < Serega` serializer
and add the `:batch` plugin once to this parent serializer.
### Plugin :root
Allows to add root key to your serialized data
Accepts options:
- :root - specifies root for all responses
- :root_one - specifies the root key for single object serialization only
- :root_many - specifies the root key for multiple objects serialization only
Adds additional config options:
- config.root.one
- config.root.many
- config.root.one=
- config.root_many=
The default root is `:data`.
The root key can be changed per serialization.
```ruby
# @example Change root per serialization:
class UserSerializer < Serega
plugin :root
end
UserSerializer.to_h(nil) # => {:data=>nil}
UserSerializer.to_h(nil, root: :user) # => {:user=>nil}
UserSerializer.to_h(nil, root: nil) # => nil
```
The root key can be removed for all responses by providing the `root: nil`
plugin option.
In this case, no root key will be added. But it still can be added manually.
```ruby
#@example Define :root plugin with different options
class UserSerializer < Serega
plugin :root # default root is :data
end
class UserSerializer < Serega
plugin :root, root: :users
end
class UserSerializer < Serega
plugin :root, root_one: :user, root_many: :people
end
class UserSerializer < Serega
plugin :root, root: nil # no root key by default
end
```
### Plugin :metadata
Depends on: [`:root`][root] plugin, that must be loaded first
Adds ability to describe metadata and adds it to serialized response
Adds class-level `.meta_attribute` method. It accepts:
- `*path` [Array of Symbols] - nested hash keys.
- `**options` [Hash]
- `:const` - describes metadata value (if it is constant)
- `:value` - describes metadata value as any `#callable` instance
- `:hide_nil` - does not show the metadata key if the value is nil.
It is `false` by default
- `:hide_empty` - does not show the metadata key if the value is nil or empty.
It is `false` by default.
- `&block` [Proc] - describes value for the current meta attribute
```ruby
class AppSerializer < Serega
plugin :root
plugin :metadata
meta_attribute(:version, const: '1.2.3')
meta_attribute(:ab_tests, :names, value: ABTests.new.method(:names))
meta_attribute(:meta, :paging, hide_nil: true) do |records, ctx|
next unless records.respond_to?(:total_count)
{
page: records.page,
per_page: records.per_page,
total_count: records.total_count
}
end
end
AppSerializer.to_h(nil)
# => {:data=>nil, :version=>"1.2.3", :ab_tests=>{:names=> ... }}
```
### Plugin :context_metadata
Depends on: [`:root`][root] plugin, that must be loaded first
Allows to provide metadata and attach it to serialized response.
Accepts option `:context_metadata_key` with the name of the root metadata keyword.
By default, it has the `:meta` value.
The key can be changed in children serializers using this method:
`config.context_metadata.key=(value)`.
```ruby
class UserSerializer < Serega
plugin :root, root: :data
plugin :context_metadata, context_metadata_key: :meta
# Same:
# plugin :context_metadata
# config.context_metadata.key = :meta
end
UserSerializer.to_h(nil, meta: { version: '1.0.1' })
# => {:data=>nil, :version=>"1.0.1"}
```
### Plugin :formatters
Allows to define `formatters` and apply them to attribute values.
Config option `config.formatters.add` can be used to add formatters.
Attribute option `:format` can be used with the name of formatter or with
callable instance.
Formatters can accept up to 2 parameters (formatted object, context)
```ruby
class AppSerializer < Serega
plugin :formatters, formatters: {
iso8601: ->(value) { time.iso8601.round(6) },
on_off: ->(value) { value ? 'ON' : 'OFF' },
money: ->(value, ctx) { value / 10**ctx[:digits) }
date: DateTypeFormatter # callable
}
end
class UserSerializer < Serega
# Additionally, we can add formatters via config in subclasses
config.formatters.add(
iso8601: ->(value) { time.iso8601.round(6) },
on_off: ->(value) { value ? 'ON' : 'OFF' },
money: ->(value) { value.round(2) }
)
# Using predefined formatter
attribute :commission, format: :money
attribute :is_logined, format: :on_off
attribute :created_at, format: :iso8601
attribute :updated_at, format: :iso8601
# Using `callable` formatter
attribute :score_percent, format: PercentFormmatter # callable class
attribute :score_percent, format: proc { |percent| "#{percent.round(2)}%" }
end
```
### Plugin :presenter
Helps to write clean code by using a Presenter class.
```ruby
class UserSerializer < Serega
plugin :presenter
attribute :name
attribute :address
class Presenter
def name
[first_name, last_name].compact_blank.join(' ')
end
def address
[country, city, address].join("\n")
end
end
end
```
### Plugin :string_modifiers
Allows to specify modifiers as strings.
Serialized attributes must be split with `,` and nested attributes must be
defined inside brackets `()`.
Modifiers can still be provided the old way using nested hashes or arrays.
```ruby
PostSerializer.plugin :string_modifiers
PostSerializer.new(only: "id,user(id,username)").to_h(post)
PostSerializer.new(except: "user(username,email)").to_h(post)
PostSerializer.new(with: "user(email)").to_h(post)
# Modifiers can still be provided the old way using nested hashes or arrays.
PostSerializer.new(with: {user: %i[email, username]}).to_h(post)
```
### Plugin :if
Plugin adds `:if, :unless, :if_value, :unless_value` options to
attributes so we can remove attributes from the response in various ways.
Use `:if` and `:unless` when you want to hide attributes before finding
attribute value, and use `:if_value` and `:unless_value` to hide attributes
after getting the final value.
Options `:if` and `:unless` accept currently serialized object and context as
parameters. Options `:if_value` and `:unless_value` accept already found
serialized value and context as parameters.
Options `:if_value` and `:unless_value` cannot be used with the `:serializer` option.
Use `:if` and `:unless` in this case.
See also a `:hide` option that is available without any plugins to hide
attribute without conditions.
Look at [select serialized fields](#selecting-fields) for `:hide` usage examples.
```ruby
class UserSerializer < Serega
attribute :email, if: :active? # translates to `if user.active?`
attribute :email, if: proc {|user| user.active?} # same
attribute :email, if: proc {|user, ctx| user == ctx[:current_user]}
attribute :email, if: CustomPolicy.method(:view_email?)
attribute :email, unless: :hidden? # translates to `unless user.hidden?`
attribute :email, unless: proc {|user| user.hidden?} # same
attribute :email, unless: proc {|user, context| context[:show_emails]}
attribute :email, unless: CustomPolicy.method(:hide_email?)
attribute :email, if_value: :present? # if email.present?
attribute :email, if_value: proc {|email| email.present?} # same
attribute :email, if_value: proc {|email, ctx| ctx[:show_emails]}
attribute :email, if_value: CustomPolicy.method(:view_email?)
attribute :email, unless_value: :blank? # unless email.blank?
attribute :email, unless_value: proc {|email| email.blank?} # same
attribute :email, unless_value: proc {|email, context| context[:show_emails]}
attribute :email, unless_value: CustomPolicy.method(:hide_email?)
end
```
### Plugin :camel_case
By default, when we add an attribute like `attribute :first_name` it means:
- adding a `:first_name` key to the resulting hash
- adding a `#first_name` method call result as value
But it's often desired to respond with *camelCased* keys.
By default, this can be achieved by specifying the attribute name and method directly
for each attribute: `attribute :firstName, method: first_name`
This plugin transforms all attribute names automatically.
We use a simple regular expression to replace `_x` with `X` for the whole string.
We make this transformation only once when the attribute is defined.
You can provide custom transformation when adding the plugin,
for example `plugin :camel_case, transform: ->(name) { name.camelize }`
For any attribute camelCase-behavior can be skipped when
the `camel_case: false` attribute option provided.
This plugin transforms only attribute keys, without affecting the `root`,
`metadata` and `context_metadata` plugins keys.
If you wish to [select serialized fields](#selecting-fields), you should
provide them camelCased.
```ruby
class AppSerializer < Serega
plugin :camel_case
end
class UserSerializer < AppSerializer
attribute :first_name
attribute :last_name
attribute :full_name, camel_case: false,
value: proc { |user| [user.first_name, user.last_name].compact.join(" ") }
end
require "ostruct"
user = OpenStruct.new(first_name: "Bruce", last_name: "Wayne")
UserSerializer.to_h(user)
# => {firstName: "Bruce", lastName: "Wayne", full_name: "Bruce Wayne"}
UserSerializer.new(only: %i[firstName lastName]).to_h(user)
# => {firstName: "Bruce", lastName: "Wayne"}
```
### Plugin :depth_limit
Helps to secure from malicious queries that serialize too much
or from accidental serializing of objects with cyclic relations.
Depth limit is checked when constructing a serialization plan, that is when
`#new` method is called, ex: `SomeSerializer.new(with: params[:with])`.
It can be useful to instantiate serializer before any other business logic
to get possible errors earlier.
Any class-level serialization methods also check the depth limit as they also
instantiate serializer.
When the depth limit is exceeded `Serega::DepthLimitError` is raised.
Depth limit error details can be found in the additional
`Serega::DepthLimitError#details` method
The limit can be checked or changed with the next config options:
- `config.depth_limit.limit`
- `config.depth_limit.limit=`
There is no default limit, but it should be set when enabling the plugin.
```ruby
class AppSerializer < Serega
plugin :depth_limit, limit: 10 # set limit for all child classes
end
class UserSerializer < AppSerializer
config.depth_limit.limit = 5 # overrides limit for UserSerializer
end
```
### Plugin :explicit_many_option
The plugin requires adding a `:many` option when adding relationships
(attributes with the `:serializer` option).
Adding this plugin makes it clearer to find if some relationship is an array or
a single object.
```ruby
class BaseSerializer < Serega
plugin :explicit_many_option
end
class UserSerializer < BaseSerializer
attribute :name
end
class PostSerializer < BaseSerializer
attribute :text
attribute :user, serializer: UserSerializer, many: false
attribute :comments, serializer: PostSerializer, many: true
end
```
## Errors
- The `Serega::SeregaError` is a base error raised by this gem.
- The `Serega::AttributeNotExist` error is raised when validating attributes in
`:only, :except, :with` modifiers. This error contains additional methods:
- `#serializer` - shows current serializer
- `#attributes` - lists not existing attributes
## Release
To release a new version, read [RELEASE.md](https://github.com/aglushkov/serega/blob/master/RELEASE.md).
## Development
- `bundle install` - install dependencies
- `bin/console` - open irb console with loaded gems
- `bundle exec rspec` - run tests
- `bundle exec rubocop` - check code standards
- `yard stats --list-undoc --no-cache` - view undocumented code
- `yard server --reload` - view code documentation
## Contributing
Bug reports, pull requests and improvements ideas are very welcome!
## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
[activerecord_preloads]: #plugin-activerecord_preloads
[batch]: #plugin-batch
[camel_case]: #plugin-camel_case
[context_metadata]: #plugin-context_metadata
[depth_limit]: #plugin-depth_limit
[formatters]: #plugin-formatters
[metadata]: #plugin-metadata
[preloads]: #plugin-preloads
[presenter]: #plugin-presenter
[root]: #plugin-root
[string_modifiers]: #plugin-string_modifiers
[if]: #plugin-if