[![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: {} }} ``` For testing purposes preloading can be done manually with `#preload_association_to(obj)` instance method ### 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