1659065640
Blueprinter is a JSON Object Presenter for Ruby that takes business objects and breaks them down into simple hashes and serializes them to JSON. It can be used in Rails in place of other serializers (like JBuilder or ActiveModelSerializers). It is designed to be simple, direct, and performant.
It heavily relies on the idea of views
which, similar to Rails views, are ways of predefining output for data in different contexts.
Docs can be found here.
Basic
If you have an object you would like serialized, simply create a blueprint. Say, for example, you have a User record with the following attributes [:uuid, :email, :first_name, :last_name, :password, :address]
.
You may define a simple blueprint like so:
class UserBlueprint < Blueprinter::Base
identifier :uuid
fields :first_name, :last_name, :email
end
and then, in your code:
puts UserBlueprint.render(user) # Output is a JSON string
And the output would look like:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"email": "john.doe@some.fake.email.domain",
"first_name": "John",
"last_name": "Doe"
}
Collections
You can also pass a collection object or an array to the render method.
puts UserBlueprint.render(User.all)
This will result in JSON that looks something like this:
[
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"email": "john.doe@some.fake.email.domain",
"first_name": "John",
"last_name": "Doe"
},
{
"uuid": "733f0758-8f21-4719-875f-743af262c3ec",
"email": "john.doe.2@some.fake.email.domain",
"first_name": "John",
"last_name": "Doe 2"
}
]
Renaming
You can rename the resulting JSON keys in both fields and associations by using the name
option.
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
association :user_projects, name: :projects
end
This will result in JSON that looks something like this:
{
"uuid": "92a5c732-2874-41e4-98fc-4123cd6cfa86",
"login": "my@email.com",
"projects": []
}
Views
You may define different outputs by utilizing views:
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
view :normal do
fields :first_name, :last_name
end
view :extended do
include_view :normal
field :address
association :projects
end
end
A view can include fields from another view by utilizing include_view
and include_views
.
Usage:
puts UserBlueprint.render(user, view: :extended)
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"address": "123 Fake St.",
"first_name": "John",
"last_name": "Doe",
"login": "john.doe@some.fake.email.domain"
}
Identifiers
identifier
s are used to specify a field or method name used as an identifier. Usually, this is something like :id
.
Example:
class UserBlueprint < Blueprinter::Base
identifier :uuid
end
Blueprinter identifier
s have a few properties that set them apart from field
s.
:identifier
view).If either of the above two developer conveniences are not desired, you can simply create your identifier fields as regular field
s.
Root
You can also optionally pass in a root key to wrap your resulting json in:
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
view :normal do
fields :first_name, :last_name
end
end
Usage:
puts UserBlueprint.render(user, view: :normal, root: :user)
Output:
{
"user": {
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"first_name": "John",
"last_name": "Doe",
"login": "john.doe@some.fake.email.domain"
}
}
Meta Attributes
You can additionally add meta-data to the json as well:
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
view :normal do
fields :first_name, :last_name
end
end
Usage:
json = UserBlueprint.render(user, view: :normal, root: :user, meta: {links: [
'https://app.mydomain.com',
'https://alternate.mydomain.com'
]})
puts json
Output:
{
"user": {
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"first_name": "John",
"last_name": "Doe",
"login": "john.doe@some.fake.email.domain"
},
"meta": {
"links": [
"https://app.mydomain.com",
"https://alternate.mydomain.com"
]
}
}
NOTE: For meta attributes, a root is mandatory.
Exclude Fields
You can specifically choose to exclude certain fields for specific views
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
view :normal do
fields :first_name, :last_name
end
view :extended do
include_view :normal
field :address
exclude :last_name
end
end
Usage:
puts UserBlueprint.render(user, view: :extended)
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"address": "123 Fake St.",
"first_name": "John",
"login": "john.doe@some.fake.email.domain"
}
Use excludes
to exclude multiple fields at once inline.
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
view :normal do
fields :age, :first_name, :last_name,
end
view :extended do
include_view :normal
field :address
excludes :age, :last_name
end
end
Associations
You may include associated objects. Say for example, a user has projects:
class ProjectBlueprint < Blueprinter::Base
identifier :uuid
field :name
end
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
view :normal do
fields :first_name, :last_name
association :projects, blueprint: ProjectBlueprint
end
end
Usage:
puts UserBlueprint.render(user, view: :normal)
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"first_name": "John",
"last_name": "Doe",
"login": "john.doe@some.fake.email.domain",
"projects": [
{
"uuid": "dca94051-4195-42bc-a9aa-eb99f7723c82",
"name": "Beach Cleanup"
},
{
"uuid": "eb881bb5-9a51-4d27-8a29-b264c30e6160",
"name": "Storefront Revamp"
}
]
}
It is also possible to pass options from one Blueprint to another via an association. For example:
class VehicleBlueprint < Blueprinter::Base
identifier :uuid
field :full_name do |vehicle, options|
"#{vehicle.model} #{options[:trim]}"
end
end
class DriverBlueprint < Blueprinter::Base
identifier :uuid
view :normal do
fields :first_name, :last_name
association :vehicles, blueprint: VehicleBlueprint, options: { trim: 'LX' }
end
end
Default Association/Field Option
By default, an association or field that evaluates to nil
is serialized as nil
. A default serialized value can be specified as an option on the association or field for cases when the association/field could potentially evaluate to nil
. You can also specify a global field_default
or association_default
in the Blueprinter config which will be used for all fields/associations that evaluate to nil.
Blueprinter.configure do |config|
config.field_default = "N/A"
config.association_default = {}
end
class UserBlueprint < Blueprinter::Base
identifier :uuid
view :normal do
field :first_name, default: "N/A"
association :company, blueprint: CompanyBlueprint, default: {}
end
end
default_if
Sometimes, you may want certain "empty" values to pass through to the default value. Blueprinter provides the ability to treat the following empty types as the default value (or nil
if no default provided).
An empty array or empty active record collection.
An empty hash.
An empty string or symbol.
class UserBlueprint < Blueprinter::Base
identifier :uuid
view :normal do
# If first_name is an empty string, it will become "N/A"
field :first_name, default_if: Blueprinter::EmptyString, default: "N/A"
# If the projects association collection is empty, it will become nil
association :projects, blueprint: ProjectBlueprint, default_if: Blueprinter::EmptyCollection
end
end
Supporting Dynamic Blueprints For Associations
When defining an association, we can dynamically evaluate the blueprint. This comes in handy when adding polymorphic associations, by allowing reuse of existing blueprints.
class Task < ActiveRecord::Base
belongs_to :taskable, polymorphic: true
end
class Project < ActiveRecord::Base
has_many :tasks, as: :taskable
def blueprint
ProjectBlueprint
end
end
class TaskBlueprint < Blueprinter::Base
identifier :uuid
view :normal do
field :title, default: "N/A"
association :taskable, blueprint: ->(taskable) {taskable.blueprint}, default: {}
end
end
NOTE: taskable.blueprint
should return a valid Blueprint class. Currently, has_many
is not supported because of the very nature of polymorphic associations.
Defining A Field Directly In The Blueprint
You can define a field directly in the Blueprint by passing it a block. This is especially useful if the object does not already have such an attribute or method defined, and you want to define it specifically for use with the Blueprint. This is done by passing field
a block. The block also yields the object and any options that were passed from render
. For example:
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :full_name do |user, options|
"#{options[:title_prefix]} #{user.first_name} #{user.last_name}"
end
end
Usage:
puts UserBlueprint.render(user, title_prefix: "Mr")
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"full_name": "Mr John Doe"
}
Defining An Identifier Directly In The Blueprint
You can also pass a block to an identifier:
class UserBlueprint < Blueprinter::Base
identifier :uuid do |user, options|
options[:current_user].anonymize(user.uuid)
end
end
Usage:
puts UserBlueprint.render(user, current_user: current_user)
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
}
Defining An Association Directly In The Blueprint
You can also pass a block to an association:
class ProjectBlueprint < Blueprinter::Base
identifier :uuid
field :name
end
class UserBlueprint < Blueprinter::Base
identifier :uuid
association :projects, blueprint: ProjectBlueprint do |user, options|
user.projects + options[:draft_projects]
end
end
Usage:
puts UserBlueprint.render(user, draft_projects: Project.where(draft: true))
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"projects": [
{"uuid": "b426a1e6-ac41-45ab-bfef-970b9a0b4289", "name": "query-console"},
{"uuid": "5bd84d6c-4fd2-4e36-ae31-c137e39be542", "name": "blueprinter"},
{"uuid": "785f5cd4-7d8d-4779-a6dd-ec5eab440eff", "name": "uncontrollable"}
]
}
Passing Additional Properties To #render
render
takes an options hash which you can pass additional properties, allowing you to utilize those additional properties in the field
block. For example:
class UserBlueprint < Blueprinter::Base
identifier :uuid
field(:company_name) do |_user, options|
options[:company].name
end
end
Usage:
puts UserBlueprint.render(user, company: company)
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"company_name": "My Company LLC"
}
Conditional Fields
Both the field
and the global Blueprinter Configuration supports :if
and :unless
options that can be used to serialize fields conditionally.
Blueprinter.configure do |config|
config.if = ->(field_name, obj, _options) { !obj[field_name].nil? }
config.unless = ->(field_name, obj, _options) { obj[field_name].nil? }
end
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :last_name, if: ->(_field_name, user, options) { user.first_name != options[:first_name] }
field :age, unless: ->(_field_name, user, _options) { user.age < 18 }
end
NOTE: The field-level setting overrides the global config setting (for the field) if both are set.
Custom Formatting for Dates and Times
To define a custom format for a Date or DateTime field, include the option datetime_format
. This global or field-level option can be either a string representing the associated strftime
format, or a Proc which receives the original Date/DateTime object and returns the formatted value. When using a Proc, it is the Proc's responsibility to handle any errors in formatting.
If a global datetime_format is set (either as a string format or a Proc), this option will be invoked and used to format all fields that respond to strftime
.
Blueprinter.configure do |config|
config.datetime_format = ->(datetime) { datetime.nil? ? datetime : datetime.strftime("%s").to_i }
end
Usage (String Option):
class UserBlueprint < Blueprinter::Base
identifier :name
field :birthday, datetime_format: "%m/%d/%Y"
end
Output:
{
"name": "John Doe",
"birthday": "03/04/1994"
}
Usage (Proc Option):
class UserBlueprint < Blueprinter::Base
identifier :name
field :birthday, datetime_format: ->(datetime) { datetime.nil? ? datetime : datetime.strftime("%s").to_i }
end
Output:
{
"name": "John Doe",
"birthday": 762739200
}
NOTE: The field-level setting overrides the global config setting (for the field) if both are set.
Transform Classes
Blueprinter provides the ability to specify transform
s on views, which enable further processing and transforming of resulting view field hashes prior to serialization.
Use transform
to specify one transformer to be included for serialization. A transformer is a class, extending Blueprinter::Transformer
and implementing the transform
method. Whatever is returned from this transform
method will end up being the resulting hash passed to serialization.
Create a Transform class extending from Blueprinter::Transformer
class DynamicFieldTransformer < Blueprinter::Transformer
def transform(hash, object, options)
hash.merge!(object.dynamic_fields)
end
end
class User
def custom_columns
self.dynamic_fields #which is an array of some columns
end
def custom_fields
custom_columns.each_with_object({}){|col,result| result[col] = self.send(col)}
end
end
Then specify the transform to use for the view.
class UserBlueprint < Blueprinter::Base
fields :first_name, :last_name
transform DynamicTransformer
end
You can also specify global default transformers. Create one or more transformer classes extending from Blueprinter::Transformer
and set the default_transformers
configuration
class LowerCamelTransformer < Blueprinter::Transformer
def transform(hash, _object, _options)
hash.transform_keys! { |key| key.to_s.camelize(:lower).to_sym }
end
end
Blueprinter.configure do |config|
config.default_transformers = [LowerCamelTransformer]
end
Note: Any transforms specified on a per-blueprint or per-view level will override the default_transformers
in the configuration.
Configurable Extractors
Blueprinter gets a given objects' values from the fields definitions using extractor classes. You can substitute your own extractor class globally or per-field.
For a specific kind of field, create an extractor class extending from Blueprinter::Extractor
class MyFieldExtractor < Blueprinter::Extractor
def extract(_field_name, _object, _local_options, _options={})
# process your obscure_object
_object.clarified
end
end
class MysteryBlueprint < Blueprinter::Base
field :obscure_object, extractor: MyFieldExtractor
end
For a global default, create an extractor class extending from Blueprinter::AutoExtractor
and set the extractor_default
configuration
class MyAutoExtractor < Blueprinter::AutoExtractor
def initialize
super
@my_field_extractor = MyFieldExtractor.new
end
def extractor(object, options)
# dispatch to any class AutoExtractor can, plus more
if detect_obscurity(object)
@my_field_extractor
else
super
end
end
end
Blueprinter.configure do |config|
config.extractor_default = MyAutoExtractor
end
Sorting Fields
By default the response sorts the keys by name. If you want the fields to be sorted in the order of definition, use the below configuration option.
Usage:
Blueprinter.configure do |config|
config.sort_fields_by = :definition
end
class UserBlueprint < Blueprinter::Base
identifier :name
field :email
field :birthday, datetime_format: "%m/%d/%Y"
end
Output:
{
"name": "John Doe",
"email": "john.doe@some.fake.email.domain",
"birthday": "03/04/1994"
}
Deprecations
When functionality in Blueprinter is invoked, that has been deprecated, the default behavior is to write a deprecation notice to stderror.
However, deprecations can be configured to report at three different levels:
Key | Result |
---|---|
:stderr (Default) | Deprecations will be written to stderror |
:raise | Deprecations will be raised as Blueprinter::BlueprinterError s |
:silence | Deprecations will be silenced and will not be raised or logged |
Blueprinter.configure do |config|
config.deprecations = :raise
end
render_as_hash
Same as render
, returns a Ruby Hash.
Usage:
puts UserBlueprint.render_as_hash(user, company: company)
Output:
{
uuid: "733f0758-8f21-4719-875f-262c3ec743af",
company_name: "My Company LLC"
}
render_as_json
Same as render
, returns a Ruby Hash JSONified. This will call JSONify all keys and values.
Usage:
puts UserBlueprint.render_as_json(user, company: company)
Output:
{
"uuid" => "733f0758-8f21-4719-875f-262c3ec743af",
"company_name" => "My Company LLC"
}
Add this line to your application's Gemfile:
gem 'blueprinter'
And then execute:
$ bundle
Or install it yourself as:
$ gem install blueprinter
You should also have require 'json'
already in your project if you are not using Rails or if you are not using Oj.
By default, Blueprinter will be calling JSON.generate(object)
internally and it expects that you have require 'json'
already in your project's code. You may use Oj
to generate in place of JSON
like so:
require 'oj' # you can skip this if OJ has already been required.
Blueprinter.configure do |config|
config.generator = Oj # default is JSON
end
Ensure that you have the Oj
gem installed in your Gemfile if you haven't already:
# Gemfile
gem 'oj'
yajl-ruby is a fast and powerful JSON generator/parser. To use yajl-ruby
in place of JSON / OJ
, use:
require 'yajl' # you can skip this if yajl has already been required.
Blueprinter.configure do |config|
config.generator = Yajl::Encoder # default is JSON
config.method = :encode # default is generate
end
NOTE: You should be doing this only if you aren't using yajl-ruby
through the JSON API by requiring yajl/json_gem
. More details here. In this case, JSON.generate
is patched to use Yajl::Encoder.encode
internally.
Feel free to browse the issues, converse, and make pull requests. If you need help, first please see if there is already an issue for your problem. Otherwise, go ahead and make a new issue.
You can run tests with bundle exec rake
.
We use Yard for documentation. Here are the following documentation rules:
@api private
.We use Yard for documentation. Here are the following documentation rules:
@api private
.To release a new version, change the version number in version.rb
, and update the CHANGELOG.md
. Finally, maintainers need to run bundle exec rake release
, which will automatically create a git tag for the version, push git commits and tags to Github, and push the .gem
file to rubygems.org.
The gem is available as open source under the terms of the MIT License.
Author: procore
Source code: https://github.com/procore/blueprinter
License: MIT license
1659065640
Blueprinter is a JSON Object Presenter for Ruby that takes business objects and breaks them down into simple hashes and serializes them to JSON. It can be used in Rails in place of other serializers (like JBuilder or ActiveModelSerializers). It is designed to be simple, direct, and performant.
It heavily relies on the idea of views
which, similar to Rails views, are ways of predefining output for data in different contexts.
Docs can be found here.
Basic
If you have an object you would like serialized, simply create a blueprint. Say, for example, you have a User record with the following attributes [:uuid, :email, :first_name, :last_name, :password, :address]
.
You may define a simple blueprint like so:
class UserBlueprint < Blueprinter::Base
identifier :uuid
fields :first_name, :last_name, :email
end
and then, in your code:
puts UserBlueprint.render(user) # Output is a JSON string
And the output would look like:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"email": "john.doe@some.fake.email.domain",
"first_name": "John",
"last_name": "Doe"
}
Collections
You can also pass a collection object or an array to the render method.
puts UserBlueprint.render(User.all)
This will result in JSON that looks something like this:
[
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"email": "john.doe@some.fake.email.domain",
"first_name": "John",
"last_name": "Doe"
},
{
"uuid": "733f0758-8f21-4719-875f-743af262c3ec",
"email": "john.doe.2@some.fake.email.domain",
"first_name": "John",
"last_name": "Doe 2"
}
]
Renaming
You can rename the resulting JSON keys in both fields and associations by using the name
option.
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
association :user_projects, name: :projects
end
This will result in JSON that looks something like this:
{
"uuid": "92a5c732-2874-41e4-98fc-4123cd6cfa86",
"login": "my@email.com",
"projects": []
}
Views
You may define different outputs by utilizing views:
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
view :normal do
fields :first_name, :last_name
end
view :extended do
include_view :normal
field :address
association :projects
end
end
A view can include fields from another view by utilizing include_view
and include_views
.
Usage:
puts UserBlueprint.render(user, view: :extended)
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"address": "123 Fake St.",
"first_name": "John",
"last_name": "Doe",
"login": "john.doe@some.fake.email.domain"
}
Identifiers
identifier
s are used to specify a field or method name used as an identifier. Usually, this is something like :id
.
Example:
class UserBlueprint < Blueprinter::Base
identifier :uuid
end
Blueprinter identifier
s have a few properties that set them apart from field
s.
:identifier
view).If either of the above two developer conveniences are not desired, you can simply create your identifier fields as regular field
s.
Root
You can also optionally pass in a root key to wrap your resulting json in:
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
view :normal do
fields :first_name, :last_name
end
end
Usage:
puts UserBlueprint.render(user, view: :normal, root: :user)
Output:
{
"user": {
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"first_name": "John",
"last_name": "Doe",
"login": "john.doe@some.fake.email.domain"
}
}
Meta Attributes
You can additionally add meta-data to the json as well:
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
view :normal do
fields :first_name, :last_name
end
end
Usage:
json = UserBlueprint.render(user, view: :normal, root: :user, meta: {links: [
'https://app.mydomain.com',
'https://alternate.mydomain.com'
]})
puts json
Output:
{
"user": {
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"first_name": "John",
"last_name": "Doe",
"login": "john.doe@some.fake.email.domain"
},
"meta": {
"links": [
"https://app.mydomain.com",
"https://alternate.mydomain.com"
]
}
}
NOTE: For meta attributes, a root is mandatory.
Exclude Fields
You can specifically choose to exclude certain fields for specific views
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
view :normal do
fields :first_name, :last_name
end
view :extended do
include_view :normal
field :address
exclude :last_name
end
end
Usage:
puts UserBlueprint.render(user, view: :extended)
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"address": "123 Fake St.",
"first_name": "John",
"login": "john.doe@some.fake.email.domain"
}
Use excludes
to exclude multiple fields at once inline.
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
view :normal do
fields :age, :first_name, :last_name,
end
view :extended do
include_view :normal
field :address
excludes :age, :last_name
end
end
Associations
You may include associated objects. Say for example, a user has projects:
class ProjectBlueprint < Blueprinter::Base
identifier :uuid
field :name
end
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
view :normal do
fields :first_name, :last_name
association :projects, blueprint: ProjectBlueprint
end
end
Usage:
puts UserBlueprint.render(user, view: :normal)
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"first_name": "John",
"last_name": "Doe",
"login": "john.doe@some.fake.email.domain",
"projects": [
{
"uuid": "dca94051-4195-42bc-a9aa-eb99f7723c82",
"name": "Beach Cleanup"
},
{
"uuid": "eb881bb5-9a51-4d27-8a29-b264c30e6160",
"name": "Storefront Revamp"
}
]
}
It is also possible to pass options from one Blueprint to another via an association. For example:
class VehicleBlueprint < Blueprinter::Base
identifier :uuid
field :full_name do |vehicle, options|
"#{vehicle.model} #{options[:trim]}"
end
end
class DriverBlueprint < Blueprinter::Base
identifier :uuid
view :normal do
fields :first_name, :last_name
association :vehicles, blueprint: VehicleBlueprint, options: { trim: 'LX' }
end
end
Default Association/Field Option
By default, an association or field that evaluates to nil
is serialized as nil
. A default serialized value can be specified as an option on the association or field for cases when the association/field could potentially evaluate to nil
. You can also specify a global field_default
or association_default
in the Blueprinter config which will be used for all fields/associations that evaluate to nil.
Blueprinter.configure do |config|
config.field_default = "N/A"
config.association_default = {}
end
class UserBlueprint < Blueprinter::Base
identifier :uuid
view :normal do
field :first_name, default: "N/A"
association :company, blueprint: CompanyBlueprint, default: {}
end
end
default_if
Sometimes, you may want certain "empty" values to pass through to the default value. Blueprinter provides the ability to treat the following empty types as the default value (or nil
if no default provided).
An empty array or empty active record collection.
An empty hash.
An empty string or symbol.
class UserBlueprint < Blueprinter::Base
identifier :uuid
view :normal do
# If first_name is an empty string, it will become "N/A"
field :first_name, default_if: Blueprinter::EmptyString, default: "N/A"
# If the projects association collection is empty, it will become nil
association :projects, blueprint: ProjectBlueprint, default_if: Blueprinter::EmptyCollection
end
end
Supporting Dynamic Blueprints For Associations
When defining an association, we can dynamically evaluate the blueprint. This comes in handy when adding polymorphic associations, by allowing reuse of existing blueprints.
class Task < ActiveRecord::Base
belongs_to :taskable, polymorphic: true
end
class Project < ActiveRecord::Base
has_many :tasks, as: :taskable
def blueprint
ProjectBlueprint
end
end
class TaskBlueprint < Blueprinter::Base
identifier :uuid
view :normal do
field :title, default: "N/A"
association :taskable, blueprint: ->(taskable) {taskable.blueprint}, default: {}
end
end
NOTE: taskable.blueprint
should return a valid Blueprint class. Currently, has_many
is not supported because of the very nature of polymorphic associations.
Defining A Field Directly In The Blueprint
You can define a field directly in the Blueprint by passing it a block. This is especially useful if the object does not already have such an attribute or method defined, and you want to define it specifically for use with the Blueprint. This is done by passing field
a block. The block also yields the object and any options that were passed from render
. For example:
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :full_name do |user, options|
"#{options[:title_prefix]} #{user.first_name} #{user.last_name}"
end
end
Usage:
puts UserBlueprint.render(user, title_prefix: "Mr")
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"full_name": "Mr John Doe"
}
Defining An Identifier Directly In The Blueprint
You can also pass a block to an identifier:
class UserBlueprint < Blueprinter::Base
identifier :uuid do |user, options|
options[:current_user].anonymize(user.uuid)
end
end
Usage:
puts UserBlueprint.render(user, current_user: current_user)
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
}
Defining An Association Directly In The Blueprint
You can also pass a block to an association:
class ProjectBlueprint < Blueprinter::Base
identifier :uuid
field :name
end
class UserBlueprint < Blueprinter::Base
identifier :uuid
association :projects, blueprint: ProjectBlueprint do |user, options|
user.projects + options[:draft_projects]
end
end
Usage:
puts UserBlueprint.render(user, draft_projects: Project.where(draft: true))
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"projects": [
{"uuid": "b426a1e6-ac41-45ab-bfef-970b9a0b4289", "name": "query-console"},
{"uuid": "5bd84d6c-4fd2-4e36-ae31-c137e39be542", "name": "blueprinter"},
{"uuid": "785f5cd4-7d8d-4779-a6dd-ec5eab440eff", "name": "uncontrollable"}
]
}
Passing Additional Properties To #render
render
takes an options hash which you can pass additional properties, allowing you to utilize those additional properties in the field
block. For example:
class UserBlueprint < Blueprinter::Base
identifier :uuid
field(:company_name) do |_user, options|
options[:company].name
end
end
Usage:
puts UserBlueprint.render(user, company: company)
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"company_name": "My Company LLC"
}
Conditional Fields
Both the field
and the global Blueprinter Configuration supports :if
and :unless
options that can be used to serialize fields conditionally.
Blueprinter.configure do |config|
config.if = ->(field_name, obj, _options) { !obj[field_name].nil? }
config.unless = ->(field_name, obj, _options) { obj[field_name].nil? }
end
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :last_name, if: ->(_field_name, user, options) { user.first_name != options[:first_name] }
field :age, unless: ->(_field_name, user, _options) { user.age < 18 }
end
NOTE: The field-level setting overrides the global config setting (for the field) if both are set.
Custom Formatting for Dates and Times
To define a custom format for a Date or DateTime field, include the option datetime_format
. This global or field-level option can be either a string representing the associated strftime
format, or a Proc which receives the original Date/DateTime object and returns the formatted value. When using a Proc, it is the Proc's responsibility to handle any errors in formatting.
If a global datetime_format is set (either as a string format or a Proc), this option will be invoked and used to format all fields that respond to strftime
.
Blueprinter.configure do |config|
config.datetime_format = ->(datetime) { datetime.nil? ? datetime : datetime.strftime("%s").to_i }
end
Usage (String Option):
class UserBlueprint < Blueprinter::Base
identifier :name
field :birthday, datetime_format: "%m/%d/%Y"
end
Output:
{
"name": "John Doe",
"birthday": "03/04/1994"
}
Usage (Proc Option):
class UserBlueprint < Blueprinter::Base
identifier :name
field :birthday, datetime_format: ->(datetime) { datetime.nil? ? datetime : datetime.strftime("%s").to_i }
end
Output:
{
"name": "John Doe",
"birthday": 762739200
}
NOTE: The field-level setting overrides the global config setting (for the field) if both are set.
Transform Classes
Blueprinter provides the ability to specify transform
s on views, which enable further processing and transforming of resulting view field hashes prior to serialization.
Use transform
to specify one transformer to be included for serialization. A transformer is a class, extending Blueprinter::Transformer
and implementing the transform
method. Whatever is returned from this transform
method will end up being the resulting hash passed to serialization.
Create a Transform class extending from Blueprinter::Transformer
class DynamicFieldTransformer < Blueprinter::Transformer
def transform(hash, object, options)
hash.merge!(object.dynamic_fields)
end
end
class User
def custom_columns
self.dynamic_fields #which is an array of some columns
end
def custom_fields
custom_columns.each_with_object({}){|col,result| result[col] = self.send(col)}
end
end
Then specify the transform to use for the view.
class UserBlueprint < Blueprinter::Base
fields :first_name, :last_name
transform DynamicTransformer
end
You can also specify global default transformers. Create one or more transformer classes extending from Blueprinter::Transformer
and set the default_transformers
configuration
class LowerCamelTransformer < Blueprinter::Transformer
def transform(hash, _object, _options)
hash.transform_keys! { |key| key.to_s.camelize(:lower).to_sym }
end
end
Blueprinter.configure do |config|
config.default_transformers = [LowerCamelTransformer]
end
Note: Any transforms specified on a per-blueprint or per-view level will override the default_transformers
in the configuration.
Configurable Extractors
Blueprinter gets a given objects' values from the fields definitions using extractor classes. You can substitute your own extractor class globally or per-field.
For a specific kind of field, create an extractor class extending from Blueprinter::Extractor
class MyFieldExtractor < Blueprinter::Extractor
def extract(_field_name, _object, _local_options, _options={})
# process your obscure_object
_object.clarified
end
end
class MysteryBlueprint < Blueprinter::Base
field :obscure_object, extractor: MyFieldExtractor
end
For a global default, create an extractor class extending from Blueprinter::AutoExtractor
and set the extractor_default
configuration
class MyAutoExtractor < Blueprinter::AutoExtractor
def initialize
super
@my_field_extractor = MyFieldExtractor.new
end
def extractor(object, options)
# dispatch to any class AutoExtractor can, plus more
if detect_obscurity(object)
@my_field_extractor
else
super
end
end
end
Blueprinter.configure do |config|
config.extractor_default = MyAutoExtractor
end
Sorting Fields
By default the response sorts the keys by name. If you want the fields to be sorted in the order of definition, use the below configuration option.
Usage:
Blueprinter.configure do |config|
config.sort_fields_by = :definition
end
class UserBlueprint < Blueprinter::Base
identifier :name
field :email
field :birthday, datetime_format: "%m/%d/%Y"
end
Output:
{
"name": "John Doe",
"email": "john.doe@some.fake.email.domain",
"birthday": "03/04/1994"
}
Deprecations
When functionality in Blueprinter is invoked, that has been deprecated, the default behavior is to write a deprecation notice to stderror.
However, deprecations can be configured to report at three different levels:
Key | Result |
---|---|
:stderr (Default) | Deprecations will be written to stderror |
:raise | Deprecations will be raised as Blueprinter::BlueprinterError s |
:silence | Deprecations will be silenced and will not be raised or logged |
Blueprinter.configure do |config|
config.deprecations = :raise
end
render_as_hash
Same as render
, returns a Ruby Hash.
Usage:
puts UserBlueprint.render_as_hash(user, company: company)
Output:
{
uuid: "733f0758-8f21-4719-875f-262c3ec743af",
company_name: "My Company LLC"
}
render_as_json
Same as render
, returns a Ruby Hash JSONified. This will call JSONify all keys and values.
Usage:
puts UserBlueprint.render_as_json(user, company: company)
Output:
{
"uuid" => "733f0758-8f21-4719-875f-262c3ec743af",
"company_name" => "My Company LLC"
}
Add this line to your application's Gemfile:
gem 'blueprinter'
And then execute:
$ bundle
Or install it yourself as:
$ gem install blueprinter
You should also have require 'json'
already in your project if you are not using Rails or if you are not using Oj.
By default, Blueprinter will be calling JSON.generate(object)
internally and it expects that you have require 'json'
already in your project's code. You may use Oj
to generate in place of JSON
like so:
require 'oj' # you can skip this if OJ has already been required.
Blueprinter.configure do |config|
config.generator = Oj # default is JSON
end
Ensure that you have the Oj
gem installed in your Gemfile if you haven't already:
# Gemfile
gem 'oj'
yajl-ruby is a fast and powerful JSON generator/parser. To use yajl-ruby
in place of JSON / OJ
, use:
require 'yajl' # you can skip this if yajl has already been required.
Blueprinter.configure do |config|
config.generator = Yajl::Encoder # default is JSON
config.method = :encode # default is generate
end
NOTE: You should be doing this only if you aren't using yajl-ruby
through the JSON API by requiring yajl/json_gem
. More details here. In this case, JSON.generate
is patched to use Yajl::Encoder.encode
internally.
Feel free to browse the issues, converse, and make pull requests. If you need help, first please see if there is already an issue for your problem. Otherwise, go ahead and make a new issue.
You can run tests with bundle exec rake
.
We use Yard for documentation. Here are the following documentation rules:
@api private
.We use Yard for documentation. Here are the following documentation rules:
@api private
.To release a new version, change the version number in version.rb
, and update the CHANGELOG.md
. Finally, maintainers need to run bundle exec rake release
, which will automatically create a git tag for the version, push git commits and tags to Github, and push the .gem
file to rubygems.org.
The gem is available as open source under the terms of the MIT License.
Author: procore
Source code: https://github.com/procore/blueprinter
License: MIT license
1622462142
Ruby on Rails is a development tool that offers Web & Mobile App Developers a structure for all the codes they write resulting in time-saving with all the common repetitive tasks during the development stage.
Want to build a Website or Mobile App with Ruby on Rails Framework
Connect with WebClues Infotech, the top Web & Mobile App development company that has served more than 600 clients worldwide. After serving them with our services WebClues Infotech is ready to serve you in fulfilling your Web & Mobile App Development Requirements.
Want to know more about development on the Ruby on Rails framework?
Visit: https://www.webcluesinfotech.com/ruby-on-rails-development/
Share your requirements https://www.webcluesinfotech.com/contact-us/
View Portfolio https://www.webcluesinfotech.com/portfolio/
#ruby on rails development services #ruby on rails development #ruby on rails web development company #ruby on rails development company #hire ruby on rails developer #hire ruby on rails developers
1626850869
Ruby on Rails is an amazing web development framework. Known for its adaptability, it powers 3,903,258 sites internationally. Ruby on Rails development speeds up the interaction within web applications. It is productive to such an extent that a Ruby on Rails developer can develop an application 25% to 40% quicker when contrasted with different frameworks.
Around 2.1% (21,034) of the best 1 million sites utilize Ruby on Rails. The framework is perfect for creating web applications in every industry. Regardless of whether it's medical services or vehicles, Rails carries a higher degree of dynamism to each application.
Be that as it may, what makes the framework so mainstream? Some say that it is affordable, some say it is on the grounds that the Ruby on Rails improvement environment is simple and basic. There are numerous reasons that make it ideal for creating dynamic applications.
Read more: Best Ruby on Rails projects Examples
There are a few other well-known backend services for web applications like Django, Flask, Laravel, and that's only the tip of the iceberg. So for what reason should organizations pick Ruby on Rails application development? We believe the accompanying reasons will feature why different organizations trust the framework -
Quick prototyping
Rails works on building MVPs in a couple of months. Organizations incline toward Ruby on Rails quick application development as it offers them more opportunity to showcase the elements. Regular development groups accomplish 25% to 40% higher efficiency when working with Rails. Joined with agile, Ruby on Rails empowers timely delivery.
Basic and simple
Ruby on Rails is easy to arrange and work with. It is not difficult to learn also. Both of these things are conceivable as a result of Ruby. The programming language has one of the most straightforward sentence structures, which is like the English language. Ruby is a universally useful programming language, working on things for web applications.
Cost-effective
Probably the greatest advantage of Rails is that it is very reasonable. The system is open-source, which implies there is no licensing charge included. Aside from that, engineers are additionally effectively accessible, that too at a lower cost. There are a large number of Ruby on Rails engineers for hire at an average compensation of $107,381 each year.
Startup-friendly
Ruby on Rails is regularly known as "the startup technology." It offers adaptable, fast, and dynamic web improvement to new companies. Most arising organizations and new businesses lean toward this as a direct result of its quick application improvement capacities. It prompts quicker MVP development, which permits new companies to rapidly search for venture investment.
Adaptable framework
Ruby on Rails is profoundly adaptable and versatile. In any event, when engineers miss adding any functions, they can utilize different modules to add highlights into the application. Aside from that, they can likewise reclassify components by eliminating or adding them during the development environment. Indeed, even individual projects can be extended and changed.
Convention over configuration
Regardless of whether it's Ruby on Rails enterprise application development or ecommerce-centered applications, the system utilizes convention over configuration. Developers don't have to go through hours attempting to set up the Ruby on Rails improvement environment. The standard conventions cover everything, improving on things for engineers on the task. The framework likewise utilizes the standard of "Don't Repeat Yourself" to guarantee there are no redundancies.
Versatile applications
At the point when organizations scale, applications regularly slack. However, this isn't the situation with Ruby on Rails web application development. The system powers sites with high traffic, It can deal with a huge load of worker demands immediately. Adaptability empowers new businesses to keep utilizing the structure even after they prepare their first model for dispatch.
Ruby on Rails is as yet a significant framework utilized by organizations all over the world - of every kind. In this day and age, it is probably the best framework to digitize endeavors through powerful web applications.
A software development company provides comprehensive Ruby on Rails development to guarantee startups and MNCs can benefit as much as possible from their digital application needs.
Reach us today for a FREE CONSULTATION.
#ruby on rails development #ruby on rails application development #ruby on rails web application development #ruby on rails developer
1659336240
In Erik Michaels-Ober's great talk, 'Writing Fast Ruby': Video @ Baruco 2014, Slide, he presented us with many idioms that lead to faster running Ruby code. He inspired me to document these to let more people know. I try to link to real commits so people can see that this can really have benefits in the real world. This does not mean you can always blindly replace one with another. It depends on the context (e.g. gsub
versus tr
). Friendly reminder: Use with caution!
Each idiom has a corresponding code example that resides in code.
All results listed in README.md are running with Ruby 2.2.0p0 on OS X 10.10.1. Machine information: MacBook Pro (Retina, 15-inch, Mid 2014), 2.5 GHz Intel Core i7, 16 GB 1600 MHz DDR3. Your results may vary, but you get the idea. : )
You can checkout the GitHub Actions build for these benchmark results ran against different Ruby implementations.
Let's write faster code, together! <3
Checkout the fasterer project - it's a static analysis that checks speed idioms written in this repo.
Use benchmark-ips (2.0+).
require "benchmark/ips"
def fast
end
def slow
end
Benchmark.ips do |x|
x.report("fast code description") { fast }
x.report("slow code description") { slow }
x.compare!
end
Parallel Assignment vs Sequential Assignment code
$ ruby -v code/general/assignment.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]
Calculating -------------------------------------
Parallel Assignment 149.201k i/100ms
Sequential Assignment
142.545k i/100ms
-------------------------------------------------
Parallel Assignment 7.687M (± 6.9%) i/s - 38.345M
Sequential Assignment
6.320M (± 8.5%) i/s - 31.360M
Comparison:
Parallel Assignment: 7686954.1 i/s
Sequential Assignment: 6320425.6 i/s - 1.22x slower
attr_accessor
vs getter and setter
code
https://www.omniref.com/ruby/2.2.0/files/method.h?#annotation=4081781&line=47
$ ruby -v code/general/attr-accessor-vs-getter-and-setter.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
getter_and_setter 61.240k i/100ms
attr_accessor 66.535k i/100ms
-------------------------------------------------
getter_and_setter 1.660M (± 9.7%) i/s - 8.267M
attr_accessor 1.865M (± 9.2%) i/s - 9.248M
Comparison:
attr_accessor: 1865408.4 i/s
getter_and_setter: 1660021.9 i/s - 1.12x slower
begin...rescue
vs respond_to?
for Control Flow code
$ ruby -v code/general/begin-rescue-vs-respond-to.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
begin...rescue 29.452k i/100ms
respond_to? 106.528k i/100ms
-------------------------------------------------
begin...rescue 371.591k (± 5.4%) i/s - 1.855M
respond_to? 3.277M (± 7.5%) i/s - 16.299M
Comparison:
respond_to?: 3276972.3 i/s
begin...rescue: 371591.0 i/s - 8.82x slower
define_method
vs module_eval
for Defining Methods code
$ ruby -v code/general/define_method-vs-module-eval.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
module_eval with string 125.000 i/100ms
define_method 138.000 i/100ms
-------------------------------------------------
module_eval with string 1.130k (±20.3%) i/s - 5.500k
define_method 1.346k (±25.9%) i/s - 6.348k
Comparison:
define_method: 1345.6 i/s
module_eval with string: 1129.7 i/s - 1.19x slower
String#constantize
vs a comparison for inflection
ActiveSupport's String#constantize "resolves the constant reference expression in its receiver".
ruby 2.7.3p183 (2021-04-05 revision 6847ee089d) [x86_64-darwin20]
Calculating -------------------------------------
using an if statement
8.124M (± 1.8%) i/s - 41.357M in 5.092437s
String#constantize 2.462M (± 2.4%) i/s - 12.315M in 5.004089s
Comparison:
using an if statement: 8123851.3 i/s
String#constantize: 2462371.2 i/s - 3.30x (± 0.00) slower
raise
vs E2MM#Raise
for raising (and defining) exeptions code
Ruby's Exception2MessageMapper module allows one to define and raise exceptions with predefined messages.
$ ruby -v code/general/raise-vs-e2mmap.rb
ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-darwin14]
Calculating -------------------------------------
Ruby exception: E2MM#Raise
2.865k i/100ms
Ruby exception: Kernel#raise
42.215k i/100ms
-------------------------------------------------
Ruby exception: E2MM#Raise
27.270k (± 8.8%) i/s - 137.520k
Ruby exception: Kernel#raise
617.446k (± 7.9%) i/s - 3.082M
Comparison:
Ruby exception: Kernel#raise: 617446.2 i/s
Ruby exception: E2MM#Raise: 27269.8 i/s - 22.64x slower
Calculating -------------------------------------
Custom exception: E2MM#Raise
2.807k i/100ms
Custom exception: Kernel#raise
45.313k i/100ms
-------------------------------------------------
Custom exception: E2MM#Raise
29.005k (± 7.2%) i/s - 145.964k
Custom exception: Kernel#raise
589.149k (± 7.8%) i/s - 2.945M
Comparison:
Custom exception: Kernel#raise: 589148.7 i/s
Custom exception: E2MM#Raise: 29004.8 i/s - 20.31x slower
loop
vs while true
code
$ ruby -v code/general/loop-vs-while-true.rb
ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-linux]
Calculating -------------------------------------
While Loop 1.000 i/100ms
Kernel loop 1.000 i/100ms
-------------------------------------------------
While Loop 0.536 (± 0.0%) i/s - 3.000 in 5.593042s
Kernel loop 0.223 (± 0.0%) i/s - 2.000 in 8.982355s
Comparison:
While Loop: 0.5 i/s
Kernel loop: 0.2 i/s - 2.41x slower
ancestors.include?
vs <=
code
$ ruby -vW0 code/general/inheritance-check.rb
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux]
Warming up --------------------------------------
less than or equal 66.992k i/100ms
ancestors.include? 16.943k i/100ms
Calculating -------------------------------------
less than or equal 1.250M (± 6.4%) i/s - 6.230M in 5.006896s
ancestors.include? 192.603k (± 4.8%) i/s - 965.751k in 5.025917s
Comparison:
less than or equal: 1249606.0 i/s
ancestors.include?: 192602.9 i/s - 6.49x slower
call
vs send
vs method_missing
code
$ ruby -v code/method/call-vs-send-vs-method_missing.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]
Calculating -------------------------------------
call 115.094k i/100ms
send 105.258k i/100ms
method_missing 100.762k i/100ms
-------------------------------------------------
call 3.811M (± 5.9%) i/s - 18.991M
send 3.244M (± 7.2%) i/s - 16.210M
method_missing 2.729M (± 9.8%) i/s - 13.401M
Comparison:
call: 3811183.4 i/s
send: 3244239.1 i/s - 1.17x slower
method_missing: 2728893.0 i/s - 1.40x slower
Normal way to apply method vs &method(...)
code
$ ruby -v code/general/block-apply-method.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]
Calculating -------------------------------------
normal 85.749k i/100ms
&method 35.529k i/100ms
-------------------------------------------------
normal 1.867M (± 7.6%) i/s - 9.347M
&method 467.095k (± 6.4%) i/s - 2.345M
Comparison:
normal: 1866669.5 i/s
&method: 467095.4 i/s - 4.00x slower
Function with single Array argument vs splat arguments code
$ ruby -v code/general/array-argument-vs-splat-arguments.rb
ruby 2.1.7p400 (2015-08-18 revision 51632) [x86_64-linux-gnu]
Calculating -------------------------------------
Function with single Array argument
157.231k i/100ms
Function with splat arguments
4.983k i/100ms
-------------------------------------------------
Function with single Array argument
5.581M (± 2.0%) i/s - 27.987M
Function with splat arguments
54.428k (± 3.3%) i/s - 274.065k
Comparison:
Function with single Array argument: 5580972.6 i/s
Function with splat arguments: 54427.7 i/s - 102.54x slower
Hash vs OpenStruct on access assuming you already have a Hash or an OpenStruct code
$ ruby -v code/general/hash-vs-openstruct-on-access.rb
ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-darwin14]
Calculating -------------------------------------
Hash 128.344k i/100ms
OpenStruct 110.723k i/100ms
-------------------------------------------------
Hash 5.279M (± 7.0%) i/s - 26.311M
OpenStruct 3.048M (± 7.0%) i/s - 15.169M
Comparison:
Hash: 5278844.0 i/s
OpenStruct: 3048139.8 i/s - 1.73x slower
Hash vs OpenStruct (creation) code
$ ruby -v code/general/hash-vs-openstruct.rb
ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-darwin14]
Calculating -------------------------------------
Hash 75.510k i/100ms
OpenStruct 9.126k i/100ms
-------------------------------------------------
Hash 1.604M (±11.0%) i/s - 7.929M
OpenStruct 96.855k (± 9.9%) i/s - 483.678k
Comparison:
Hash: 1604259.1 i/s
OpenStruct: 96855.3 i/s - 16.56x slower
Kernel#format vs Float#round().to_s code
$ ruby -v code/general/format-vs-round-and-to-s.rb
ruby 2.3.3p222 (2016-11-21 revision 56859) [x86_64-darwin15]
Warming up --------------------------------------
Float#round 106.645k i/100ms
Kernel#format 84.304k i/100ms
String#% 78.635k i/100ms
Calculating -------------------------------------
Float#round 1.570M (± 3.2%) i/s - 7.892M in 5.030672s
Kernel#format 1.144M (± 3.0%) i/s - 5.733M in 5.015621s
String#% 1.047M (± 4.2%) i/s - 5.269M in 5.042970s
Comparison:
Float#round: 1570411.4 i/s
Kernel#format: 1144036.6 i/s - 1.37x slower
String#%: 1046689.1 i/s - 1.50x slower
Array#bsearch
vs Array#find
code
WARNING: bsearch
ONLY works on sorted array. More details please see #29.
$ ruby -v code/array/bsearch-vs-find.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
find 1.000 i/100ms
bsearch 42.216k i/100ms
-------------------------------------------------
find 0.184 (± 0.0%) i/s - 1.000 in 5.434758s
bsearch 577.301k (± 6.6%) i/s - 2.913M
Comparison:
bsearch: 577300.7 i/s
find: 0.2 i/s - 3137489.63x slower
Array#length
vs Array#size
vs Array#count
code
Use #length
when you only want to know how many elements in the array, #count
could also achieve this. However #count
should be use for counting specific elements in array. Note #size
is an alias of #length
.
$ ruby -v code/array/length-vs-size-vs-count.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]
Calculating -------------------------------------
Array#length 172.998k i/100ms
Array#size 168.130k i/100ms
Array#count 164.911k i/100ms
-------------------------------------------------
Array#length 11.394M (± 6.1%) i/s - 56.743M
Array#size 11.303M (± 6.5%) i/s - 56.324M
Array#count 9.195M (± 8.6%) i/s - 45.680M
Comparison:
Array#length: 11394036.7 i/s
Array#size: 11302701.1 i/s - 1.01x slower
Array#count: 9194976.2 i/s - 1.24x slower
Array#shuffle.first
vs Array#sample
code
Array#shuffle
allocates an extra array.Array#sample
indexes into the array without allocating an extra array.
This is the reason why Array#sample exists.
—— @sferik rails/rails#17245
$ ruby -v code/array/shuffle-first-vs-sample.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
Array#shuffle.first 25.406k i/100ms
Array#sample 125.101k i/100ms
-------------------------------------------------
Array#shuffle.first 304.341k (± 4.3%) i/s - 1.524M
Array#sample 5.727M (± 8.6%) i/s - 28.523M
Comparison:
Array#sample: 5727032.0 i/s
Array#shuffle.first: 304341.1 i/s - 18.82x slower
Array#[](0)
vs Array#first
code
$ ruby -v code/array/array-first-vs-index.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
Array#[0] 152.751k i/100ms
Array#first 148.088k i/100ms
-------------------------------------------------
Array#[0] 8.614M (± 7.0%) i/s - 42.923M
Array#first 7.465M (±10.7%) i/s - 36.874M
Comparison:
Array#[0]: 8613583.7 i/s
Array#first: 7464526.6 i/s - 1.15x slower
Array#[](-1)
vs Array#last
code
$ ruby -v code/array/array-last-vs-index.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
Array#[-1] 151.940k i/100ms
Array#last 153.371k i/100ms
-------------------------------------------------
Array#[-1] 8.582M (± 4.6%) i/s - 42.847M
Array#last 7.639M (± 5.7%) i/s - 38.189M
Comparison:
Array#[-1]: 8582074.3 i/s
Array#last: 7639254.5 i/s - 1.12x slower
Array#insert
vs Array#unshift
code
$ ruby -v code/array/insert-vs-unshift.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin10.0]
Calculating -------------------------------------
Array#unshift 4.000 i/100ms
Array#insert 1.000 i/100ms
-------------------------------------------------
Array#unshift 44.947 (± 6.7%) i/s - 224.000
Array#insert 0.171 (± 0.0%) i/s - 1.000 in 5.841595s
Comparison:
Array#unshift: 44.9 i/s
Array#insert: 0.2 i/s - 262.56x slower
Array#new
vs Fixnum#times + map
code
Typical slowdown is 40-60% depending on the size of the array. See the corresponding pull request for performance characteristics.
ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-darwin15]
Calculating -------------------------------------
Array#new 63.875k i/100ms
Fixnum#times + map 48.010k i/100ms
-------------------------------------------------
Array#new 1.070M (± 2.2%) i/s - 5.365M
Fixnum#times + map 678.097k (± 2.7%) i/s - 3.409M
Comparison:
Array#new: 1069837.0 i/s
Fixnum#times + map: 678097.4 i/s - 1.58x slower
Enumerable#each + push
vs Enumerable#map
code
$ ruby -v code/enumerable/each-push-vs-map.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
Array#each + push 9.025k i/100ms
Array#map 13.947k i/100ms
-------------------------------------------------
Array#each + push 99.634k (± 3.2%) i/s - 505.400k
Array#map 158.091k (± 4.2%) i/s - 794.979k
Comparison:
Array#map: 158090.9 i/s
Array#each + push: 99634.2 i/s - 1.59x slower
Enumerable#each
vs for
loop code
$ ruby -v code/enumerable/each-vs-for-loop.rb
ruby 2.2.0preview1 (2014-09-17 trunk 47616) [x86_64-darwin14]
Calculating -------------------------------------
For loop 17.111k i/100ms
#each 18.464k i/100ms
-------------------------------------------------
For loop 198.517k (± 5.3%) i/s - 992.438k
#each 208.157k (± 5.0%) i/s - 1.052M
Comparison:
#each: 208157.4 i/s
For loop: 198517.3 i/s - 1.05x slower
Enumerable#each_with_index
vs while
loop code
$ ruby -v code/enumerable/each_with_index-vs-while-loop.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
While Loop 22.553k i/100ms
each_with_index 11.963k i/100ms
-------------------------------------------------
While Loop 240.752k (± 7.1%) i/s - 1.218M
each_with_index 126.753k (± 5.9%) i/s - 634.039k
Comparison:
While Loop: 240752.1 i/s
each_with_index: 126753.4 i/s - 1.90x slower
Enumerable#map
...Array#flatten
vs Enumerable#flat_map
code
-- @sferik rails/rails@3413b88, Replace map.flatten with flat_map, Replace map.flatten(1) with flat_map
$ ruby -v code/enumerable/map-flatten-vs-flat_map.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
Array#map.flatten(1) 3.315k i/100ms
Array#map.flatten 3.283k i/100ms
Array#flat_map 5.350k i/100ms
-------------------------------------------------
Array#map.flatten(1) 33.801k (± 4.3%) i/s - 169.065k
Array#map.flatten 34.530k (± 6.0%) i/s - 173.999k
Array#flat_map 55.980k (± 5.0%) i/s - 283.550k
Comparison:
Array#flat_map: 55979.6 i/s
Array#map.flatten: 34529.6 i/s - 1.62x slower
Array#map.flatten(1): 33800.6 i/s - 1.66x slower
Enumerable#reverse.each
vs Enumerable#reverse_each
code
Enumerable#reverse
allocates an extra array.Enumerable#reverse_each
yields each value without allocating an extra array.
This is the reason whyEnumerable#reverse_each
exists.
-- @sferik rails/rails#17244
$ ruby -v code/enumerable/reverse-each-vs-reverse_each.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
Array#reverse.each 16.746k i/100ms
Array#reverse_each 18.590k i/100ms
-------------------------------------------------
Array#reverse.each 190.729k (± 4.8%) i/s - 954.522k
Array#reverse_each 216.060k (± 4.3%) i/s - 1.078M
Comparison:
Array#reverse_each: 216060.5 i/s
Array#reverse.each: 190729.1 i/s - 1.13x slower
Enumerable#sort_by.first
vs Enumerable#min_by
code
Enumerable#sort_by
performs a sort of the enumerable and allocates a new array the size of the enumerable. Enumerable#min_by
doesn't perform a sort or allocate an array the size of the enumerable. Similar comparisons hold for Enumerable#sort_by.last
vs Enumerable#max_by
, Enumerable#sort.first
vs Enumerable#min
, and Enumerable#sort.last
vs Enumerable#max
.
$ ruby -v code/enumerable/sort_by-first-vs-min_by.rb
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]
Warming up --------------------------------------
Enumerable#min_by 15.170k i/100ms
Enumerable#sort_by...first
10.413k i/100ms
Calculating -------------------------------------
Enumerable#min_by 157.877k (± 0.9%) i/s - 804.010k in 5.093048s
Enumerable#sort_by...first
106.831k (± 1.3%) i/s - 541.476k in 5.069403s
Comparison:
Enumerable#min_by: 157877.0 i/s
Enumerable#sort_by...first: 106831.1 i/s - 1.48x slower
Enumerable#detect
vs Enumerable#select.first
code
$ ruby -v code/enumerable/select-first-vs-detect.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
Enumerable#select.first 8.515k i/100ms
Enumerable#detect 33.885k i/100ms
-------------------------------------------------
Enumerable#select.first 89.757k (± 5.0%) i/s - 1.797M
Enumerable#detect 434.304k (± 5.2%) i/s - 8.675M
Comparison:
Enumerable#detect: 434304.2 i/s
Enumerable#select.first: 89757.4 i/s - 4.84x slower
Enumerable#select.last
vs Enumerable#reverse.detect
code
$ ruby -v code/enumerable/select-last-vs-reverse-detect.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
Enumerable#reverse.detect 62.636k i/100ms
Enumerable#select.last 11.687k i/100ms
-------------------------------------------------
Enumerable#reverse.detect 1.263M (± 8.2%) i/s - 6.326M
Enumerable#select.last 119.387k (± 5.7%) i/s - 596.037k
Comparison:
Enumerable#reverse.detect: 1263100.2 i/s
Enumerable#select.last: 119386.8 i/s - 10.58x slower
Enumerable#sort
vs Enumerable#sort_by
code
$ ruby -v code/enumerable/sort-vs-sort_by.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]
Calculating -------------------------------------
Enumerable#sort_by (Symbol#to_proc) 2.680k i/100ms
Enumerable#sort_by 2.462k i/100ms
Enumerable#sort 1.320k i/100ms
-------------------------------------------------
Enumerable#sort_by (Symbol#to_proc) 25.916k (± 4.4%) i/s - 131.320k
Enumerable#sort_by 24.650k (± 5.1%) i/s - 125.562k
Enumerable#sort 14.018k (± 5.6%) i/s - 69.960k
Comparison:
Enumerable#sort_by (Symbol#to_proc): 25916.1 i/s
Enumerable#sort_by: 24650.2 i/s - 1.05x slower
Enumerable#sort: 14018.3 i/s - 1.85x slower
Enumerable#inject Symbol
vs Enumerable#inject Proc
code
Of note, to_proc
for 1.8.7 is considerable slower than the block format
$ ruby -v code/enumerable/inject-symbol-vs-block.rb
ruby 2.2.4p230 (2015-12-16 revision 53155) [x86_64-darwin14]
Warming up --------------------------------------
inject symbol 1.893k i/100ms
inject to_proc 1.583k i/100ms
inject block 1.390k i/100ms
Calculating -------------------------------------
inject symbol 19.001k (± 3.8%) i/s - 96.543k
inject to_proc 15.958k (± 3.5%) i/s - 80.733k
inject block 14.063k (± 3.9%) i/s - 70.890k
Comparison:
inject symbol: 19001.5 i/s
inject to_proc: 15958.3 i/s - 1.19x slower
inject block: 14063.1 i/s - 1.35x slower
Date.iso8601
vs Date.parse
code
When expecting well-formatted data from e.g. an API, iso8601
is faster and will raise an ArgumentError
on malformed input.
$ ruby -v code/date/iso8601-vs-parse.rb
ruby 2.4.3p205 (2017-12-14 revision 61247) [x86_64-darwin17]
Warming up --------------------------------------
Date.iso8601 28.880k i/100ms
Date.parse 15.805k i/100ms
Calculating -------------------------------------
Date.iso8601 328.035k (± 4.7%) i/s - 1.646M in 5.029287s
Date.parse 175.546k (± 3.8%) i/s - 885.080k in 5.049444s
Comparison:
Date.iso8601: 328035.3 i/s
Date.parse: 175545.9 i/s - 1.87x slower
Hash#[]
vs Hash#fetch
code
If you use Ruby 2.2, Symbol
could be more performant than String
as Hash
keys. Read more regarding this: Symbol GC in Ruby 2.2 and Unraveling String Key Performance in Ruby 2.2.
$ ruby -v code/hash/bracket-vs-fetch.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]
Calculating -------------------------------------
Hash#[], symbol 143.850k i/100ms
Hash#fetch, symbol 137.425k i/100ms
Hash#[], string 143.083k i/100ms
Hash#fetch, string 120.417k i/100ms
-------------------------------------------------
Hash#[], symbol 7.531M (± 6.6%) i/s - 37.545M
Hash#fetch, symbol 6.644M (± 8.2%) i/s - 32.982M
Hash#[], string 6.657M (± 7.7%) i/s - 33.195M
Hash#fetch, string 3.981M (± 8.7%) i/s - 19.748M
Comparison:
Hash#[], symbol: 7531355.8 i/s
Hash#[], string: 6656818.8 i/s - 1.13x slower
Hash#fetch, symbol: 6643665.5 i/s - 1.13x slower
Hash#fetch, string: 3981166.5 i/s - 1.89x slower
Hash#dig
vs Hash#[]
vs Hash#fetch
code
Ruby 2.3 introduced Hash#dig
which is a readable and performant option for retrieval from a nested hash, returning nil
if an extraction step fails. See #102 (comment) for more info.
$ ruby -v code/hash/dig-vs-\[\]-vs-fetch.rb
ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-darwin15]
Calculating -------------------------------------
Hash#dig 5.719M (± 6.1%) i/s - 28.573M in 5.013997s
Hash#[] 6.066M (± 6.9%) i/s - 30.324M in 5.025614s
Hash#[] || 5.366M (± 6.5%) i/s - 26.933M in 5.041403s
Hash#[] && 2.782M (± 4.8%) i/s - 13.905M in 5.010328s
Hash#fetch 4.101M (± 6.1%) i/s - 20.531M in 5.024945s
Hash#fetch fallback 2.975M (± 5.5%) i/s - 14.972M in 5.048880s
Comparison:
Hash#[]: 6065791.0 i/s
Hash#dig: 5719290.9 i/s - same-ish: difference falls within error
Hash#[] ||: 5366226.5 i/s - same-ish: difference falls within error
Hash#fetch: 4101102.1 i/s - 1.48x slower
Hash#fetch fallback: 2974906.9 i/s - 2.04x slower
Hash#[] &&: 2781646.6 i/s - 2.18x slower
Hash[]
vs Hash#dup
code
Source: http://tenderlovemaking.com/2015/02/11/weird-stuff-with-hashes.html
Does this mean that you should switch to Hash[]? Only if your benchmarks can prove that it’s a bottleneck. Please please please don’t change all of your code because this shows it’s faster. Make sure to measure your app performance first.
$ ruby -v code/hash/bracket-vs-dup.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
Hash[] 29.403k i/100ms
Hash#dup 16.195k i/100ms
-------------------------------------------------
Hash[] 343.987k (± 8.7%) i/s - 1.735M
Hash#dup 163.516k (±10.2%) i/s - 825.945k
Comparison:
Hash[]: 343986.5 i/s
Hash#dup: 163516.3 i/s - 2.10x slower
Hash#fetch
with argument vs Hash#fetch
+ block code
Note that the speedup in the block version comes from avoiding repeated
construction of the argument. If the argument is a constant, number symbol or
something of that sort the argument version is actually slightly faster
See also #39 (comment)
$ ruby -v code/hash/fetch-vs-fetch-with-block.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin13]
Calculating -------------------------------------
Hash#fetch + const 129.868k i/100ms
Hash#fetch + block 125.254k i/100ms
Hash#fetch + arg 121.155k i/100ms
-------------------------------------------------
Hash#fetch + const 7.031M (± 7.0%) i/s - 34.934M
Hash#fetch + block 6.815M (± 4.2%) i/s - 34.069M
Hash#fetch + arg 4.753M (± 5.6%) i/s - 23.746M
Comparison:
Hash#fetch + const: 7030600.4 i/s
Hash#fetch + block: 6814826.7 i/s - 1.03x slower
Hash#fetch + arg: 4752567.2 i/s - 1.48x slower
Hash#each_key
instead of Hash#keys.each
code
Hash#keys.each
allocates an array of keys;Hash#each_key
iterates through the keys without allocating a new array.
This is the reason whyHash#each_key
exists.
—— @sferik rails/rails#17099
$ ruby -v code/hash/keys-each-vs-each_key.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
Hash#keys.each 56.690k i/100ms
Hash#each_key 59.658k i/100ms
-------------------------------------------------
Hash#keys.each 869.262k (± 5.0%) i/s - 4.365M
Hash#each_key 1.049M (± 6.0%) i/s - 5.250M
Comparison:
Hash#each_key: 1049161.6 i/s
Hash#keys.each: 869262.3 i/s - 1.21x slower
Hash#key?
instead of Hash#keys.include?
code
Hash#keys.include?
allocates an array of keys and performs an O(n) search;Hash#key?
performs an O(1) hash lookup without allocating a new array.
$ ruby -v code/hash/keys-include-vs-key.rb
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]
Calculating -------------------------------------
Hash#keys.include? 8.612k (± 2.5%) i/s - 43.248k in 5.024749s
Hash#key? 6.366M (± 5.5%) i/s - 31.715M in 5.002276s
Comparison:
Hash#key?: 6365855.5 i/s
Hash#keys.include?: 8612.4 i/s - 739.15x slower
Hash#value?
instead of Hash#values.include?
code
Hash#values.include?
allocates an array of values and performs an O(n) search;Hash#value?
performs an O(n) search without allocating a new array.
$ ruby -v code/hash/values-include-vs-value.rb
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]
Calculating -------------------------------------
Hash#values.include? 23.187k (± 4.3%) i/s - 117.720k in 5.086976s
Hash#value? 38.395k (± 1.0%) i/s - 194.361k in 5.062696s
Comparison:
Hash#value?: 38395.0 i/s
Hash#values.include?: 23186.8 i/s - 1.66x slower
Hash#merge!
vs Hash#[]=
code
$ ruby -v code/hash/merge-bang-vs-\[\]=.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
Hash#merge! 1.023k i/100ms
Hash#[]= 2.844k i/100ms
-------------------------------------------------
Hash#merge! 10.653k (± 4.9%) i/s - 53.196k
Hash#[]= 28.287k (±12.4%) i/s - 142.200k
Comparison:
Hash#[]=: 28287.1 i/s
Hash#merge!: 10653.3 i/s - 2.66x slower
Hash#update
vs Hash#[]=
code
$ ruby -v code/hash/update-vs-\[\]=.rb
ruby 2.6.6p146 (2020-03-31 revision 67876) [x86_64-darwin18]
Warming up --------------------------------------
Hash#[]= 7.453k i/100ms
Hash#update 4.311k i/100ms
Calculating -------------------------------------
Hash#[]= 74.764k (± 1.9%) i/s - 380.103k in 5.085962s
Hash#update 43.220k (± 0.8%) i/s - 219.861k in 5.087364s
Comparison:
Hash#[]=: 74764.0 i/s
Hash#update: 43220.1 i/s - 1.73x (± 0.00) slower
Hash#merge
vs Hash#**other
code
$ ruby -v code/hash/merge-vs-double-splat-operator.rb
ruby 2.3.3p222 (2016-11-21 revision 56859) [x86_64-darwin15]
Warming up --------------------------------------
Hash#**other 64.624k i/100ms
Hash#merge 38.827k i/100ms
Calculating -------------------------------------
Hash#**other 798.397k (± 6.9%) i/s - 4.007M in 5.053516s
Hash#merge 434.171k (± 4.5%) i/s - 2.174M in 5.018927s
Comparison:
Hash#**other: 798396.6 i/s
Hash#merge: 434170.8 i/s - 1.84x slower
Hash#merge
vs Hash#merge!
code
$ ruby -v code/hash/merge-vs-merge-bang.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
Hash#merge 39.000 i/100ms
Hash#merge! 1.008k i/100ms
-------------------------------------------------
Hash#merge 409.610 (± 7.6%) i/s - 2.067k
Hash#merge! 9.830k (± 5.8%) i/s - 49.392k
Comparison:
Hash#merge!: 9830.3 i/s
Hash#merge: 409.6 i/s - 24.00x slower
{}#merge!(Hash)
vs Hash#merge({})
vs Hash#dup#merge!({})
code
When we don't want to modify the original hash, and we want duplicates to be created
See #42 for more details.
$ ruby -v code/hash/merge-bang-vs-merge-vs-dup-merge-bang.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux]
Calculating -------------------------------------
{}#merge!(Hash) do end 2.006k i/100ms
Hash#merge({}) 762.000 i/100ms
Hash#dup#merge!({}) 736.000 i/100ms
-------------------------------------------------
{}#merge!(Hash) do end 20.055k (± 2.0%) i/s - 100.300k in 5.003322s
Hash#merge({}) 7.676k (± 1.2%) i/s - 38.862k in 5.063382s
Hash#dup#merge!({}) 7.440k (± 1.1%) i/s - 37.536k in 5.045851s
Comparison:
{}#merge!(Hash) do end: 20054.8 i/s
Hash#merge({}): 7676.3 i/s - 2.61x slower
Hash#dup#merge!({}): 7439.9 i/s - 2.70x slower
Hash#sort_by
vs Hash#sort
code
To sort hash by key.
$ ruby -v code/hash/hash-key-sort_by-vs-sort.rb
ruby 2.2.1p85 (2015-02-26 revision 49769) [x86_64-darwin14]
Calculating -------------------------------------
sort_by + to_h 11.468k i/100ms
sort + to_h 8.107k i/100ms
-------------------------------------------------
sort_by + to_h 122.176k (± 6.0%) i/s - 619.272k
sort + to_h 81.973k (± 4.7%) i/s - 413.457k
Comparison:
sort_by + to_h: 122176.2 i/s
sort + to_h: 81972.8 i/s - 1.49x slower
Native Hash#slice
vs other slice implementations before native code
Since ruby 2.5, Hash comes with a slice
method to select hash members by keys.
$ ruby -v code/hash/slice-native-vs-before-native.rb
ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-linux]
Warming up --------------------------------------
Hash#native-slice 178.077k i/100ms
Array#each 124.311k i/100ms
Array#each_w/_object 110.818k i/100ms
Hash#select-include 66.972k i/100ms
Calculating -------------------------------------
Hash#native-slice 2.540M (± 1.5%) i/s - 12.822M in 5.049955s
Array#each 1.614M (± 1.0%) i/s - 8.080M in 5.007925s
Array#each_w/_object 1.353M (± 2.6%) i/s - 6.760M in 5.000441s
Hash#select-include 760.944k (± 0.9%) i/s - 3.817M in 5.017123s
Comparison:
Hash#native-slice : 2539515.5 i/s
Array#each : 1613665.5 i/s - 1.57x slower
Array#each_w/_object: 1352851.8 i/s - 1.88x slower
Hash#select-include : 760944.2 i/s - 3.34x slower
Block vs Symbol#to_proc
code
Symbol#to_proc
is considerably more concise than using block syntax.
...In some cases, it reduces the number of lines of code.
—— @sferik rails/rails#16833
$ ruby -v code/proc-and-block/block-vs-to_proc.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
Block 4.632k i/100ms
Symbol#to_proc 5.225k i/100ms
-------------------------------------------------
Block 47.914k (± 6.3%) i/s - 240.864k
Symbol#to_proc 54.791k (± 4.1%) i/s - 276.925k
Comparison:
Symbol#to_proc: 54791.1 i/s
Block: 47914.3 i/s - 1.14x slower
Proc#call
and block arguments vs yield
code
In MRI Ruby before 2.5, block arguments are converted to Procs, which incurs a heap allocation.
$ ruby -v code/proc-and-block/proc-call-vs-yield.rb
ruby 2.4.4p296 (2018-03-28 revision 63013) [x86_64-darwin18]
Calculating -------------------------------------
block.call 1.967M (± 2.0%) i/s - 9.871M in 5.019328s
block + yield 2.147M (± 3.3%) i/s - 10.814M in 5.044319s
unused block 2.265M (± 1.9%) i/s - 11.333M in 5.004522s
yield 10.436M (± 1.6%) i/s - 52.260M in 5.008851s
Comparison:
yield: 10436414.0 i/s
unused block: 2265399.0 i/s - 4.61x slower
block + yield: 2146619.0 i/s - 4.86x slower
block.call: 1967300.9 i/s - 5.30x slower
MRI Ruby 2.5 implements Lazy Proc allocation for block parameters:
$ ruby -v code/proc-and-block/proc-call-vs-yield.rb
ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-darwin18]
Calculating -------------------------------------
block.call 1.970M (± 2.3%) i/s - 9.863M in 5.009599s
block + yield 9.075M (± 2.6%) i/s - 45.510M in 5.018369s
unused block 11.176M (± 2.7%) i/s - 55.977M in 5.012741s
yield 10.588M (± 1.9%) i/s - 53.108M in 5.017755s
Comparison:
unused block: 11176355.0 i/s
yield: 10588342.3 i/s - 1.06x slower
block + yield: 9075355.5 i/s - 1.23x slower
block.call: 1969834.0 i/s - 5.67x slower
MRI Ruby 2.6 implements an optimization for block.call where a block parameter is passed:
$ ruby -v code/proc-and-block/proc-call-vs-yield.rb
ruby 2.6.1p33 (2019-01-30 revision 66950) [x86_64-darwin18]
Calculating -------------------------------------
block.call 10.587M (± 1.2%) i/s - 52.969M in 5.003808s
block + yield 12.630M (± 0.3%) i/s - 63.415M in 5.020910s
unused block 15.981M (± 0.8%) i/s - 80.255M in 5.022305s
yield 15.352M (± 3.1%) i/s - 76.816M in 5.009404s
Comparison:
unused block: 15980789.4 i/s
yield: 15351931.0 i/s - 1.04x slower
block + yield: 12630378.1 i/s - 1.27x slower
block.call: 10587315.1 i/s - 1.51x slower
String#dup
vs String#+
code
Note that String.new
is not the same as the options compared, since it is always ASCII-8BIT
encoded instead of the script encoding (usually UTF-8
).
$ ruby -v code/string/dup-vs-unary-plus.rb
ruby 2.4.3p205 (2017-12-14 revision 61247) [x86_64-darwin17]
Calculating -------------------------------------
String#+@ 7.697M (± 1.4%) i/s - 38.634M in 5.020313s
String#dup 3.566M (± 1.0%) i/s - 17.860M in 5.008377s
Comparison:
String#+@: 7697108.3 i/s
String#dup: 3566485.7 i/s - 2.16x slower
String#casecmp
vs String#casecmp?
vs String#downcase + ==
code
String#casecmp?
is available on Ruby 2.4 or later. Note that String#casecmp
only works on characters A-Z/a-z, not all of Unicode.
$ ruby -v code/string/casecmp-vs-downcase-\=\=.rb
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin19]
Warming up --------------------------------------
String#casecmp? 395.796k i/100ms
String#downcase + == 543.958k i/100ms
String#casecmp 730.028k i/100ms
Calculating -------------------------------------
String#casecmp? 3.687M (±10.9%) i/s - 18.602M in 5.158065s
String#downcase + == 5.017M (±11.3%) i/s - 25.022M in 5.089175s
String#casecmp 6.948M (± 6.0%) i/s - 35.041M in 5.062714s
Comparison:
String#casecmp: 6948231.0 i/s
String#downcase + ==: 5017089.5 i/s - 1.38x (± 0.00) slower
String#casecmp?: 3686650.7 i/s - 1.88x (± 0.00) slower
String Concatenation code
$ ruby -v code/string/concatenation.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux]
Warming up --------------------------------------
String#+ 149.298k i/100ms
String#concat 151.505k i/100ms
String#append 153.389k i/100ms
"foo" "bar" 195.552k i/100ms
"#{'foo'}#{'bar'}" 193.784k i/100ms
Calculating -------------------------------------
String#+ 2.977M (± 1.1%) i/s - 14.930M in 5.015179s
String#concat 3.017M (± 1.3%) i/s - 15.150M in 5.023063s
String#append 3.076M (± 1.2%) i/s - 15.492M in 5.037683s
"foo" "bar" 5.370M (± 1.0%) i/s - 26.986M in 5.026271s
"#{'foo'}#{'bar'}" 5.182M (± 4.6%) i/s - 25.967M in 5.022093s
Comparison:
"foo" "bar": 5369594.5 i/s
"#{'foo'}#{'bar'}": 5181745.7 i/s - same-ish: difference falls within error
String#append: 3075719.2 i/s - 1.75x slower
String#concat: 3016703.5 i/s - 1.78x slower
String#+: 2977282.7 i/s - 1.80x slower
String#match
vs String.match?
vs String#start_with?
/String#end_with?
code (start) code (end)
The regular expression approaches become slower as the tested string becomes longer. For short strings, String#match?
performs similarly to String#start_with?
/String#end_with?
.
:warning:
Sometimes you cant replace regexp withstart_with?
,
for example:"a\nb" =~ /^b/ #=> 2
but"a\nb" =~ /\Ab/ #=> nil
.
:warning:
You can combinestart_with?
andend_with?
to replaceerror.path =~ /^#{path}(\.rb)?$/
to thiserror.path.start_with?(path) && error.path.end_with?('.rb', '')
—— @igas rails/rails#17316
$ ruby -v code/string/start-string-checking-match-vs-start_with.rb
ruby 2.4.3p205 (2017-12-14 revision 61247) [x86_64-darwin17]
Calculating -------------------------------------
String#=~ 1.088M (± 4.0%) i/s - 5.471M in 5.034404s
String#match? 5.138M (± 5.0%) i/s - 25.669M in 5.008810s
String#start_with? 6.314M (± 4.3%) i/s - 31.554M in 5.007207s
Comparison:
String#start_with?: 6314182.0 i/s
String#match?: 5138115.1 i/s - 1.23x slower
String#=~: 1088461.5 i/s - 5.80x slower
$ ruby -v code/string/end-string-checking-match-vs-end_with.rb
ruby 2.4.3p205 (2017-12-14 revision 61247) [x86_64-darwin17]
Calculating -------------------------------------
String#=~ 918.101k (± 6.0%) i/s - 4.650M in 5.084079s
String#match? 3.009M (± 6.8%) i/s - 14.991M in 5.005691s
String#end_with? 4.548M (± 9.3%) i/s - 22.684M in 5.034115s
Comparison:
String#end_with?: 4547871.0 i/s
String#match?: 3008554.5 i/s - 1.51x slower
String#=~: 918100.5 i/s - 4.95x slower
String#start_with?
vs String#[].==
code
$ ruby -v code/string/end-string-checking-match-vs-end_with.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]
Calculating -------------------------------------
String#start_with? 2.047M (± 4.5%) i/s - 10.242M in 5.015146s
String#[0, n] == 711.802k (± 7.3%) i/s - 3.551M in 5.019543s
String#[RANGE] == 651.751k (± 6.2%) i/s - 3.296M in 5.078772s
String#[0...n] == 427.207k (± 5.7%) i/s - 2.136M in 5.019245s
Comparison:
String#start_with?: 2046618.9 i/s
String#[0, n] ==: 711802.3 i/s - 2.88x slower
String#[RANGE] ==: 651751.2 i/s - 3.14x slower
String#[0...n] ==: 427206.8 i/s - 4.79x slower
Regexp#===
vs String#match
vs String#=~
vs String#match?
code
String#match?
and Regexp#match?
are available on Ruby 2.4 or later. ActiveSupport provides a forward compatible extension of Regexp
for older Rubies without the speed improvement.
:warning:
Sometimes you can't replacematch
withmatch?
,
This is only useful for cases where you are checking
for a match and not using the resultant match object.
:warning:Regexp#===
is also faster thanString#match
but you need to switch the order of arguments.
$ ruby -v code/string/===-vs-=~-vs-match.rb
ruby 2.4.3p205 (2017-12-14 revision 61247) [x86_64-darwin17]
Calculating -------------------------------------
String#match? 6.284M (± 5.6%) i/s - 31.324M in 5.001471s
String#=~ 2.581M (± 4.7%) i/s - 12.977M in 5.038887s
Regexp#=== 2.482M (± 4.1%) i/s - 12.397M in 5.002808s
String#match 2.097M (± 4.3%) i/s - 10.592M in 5.060535s
Comparison:
String#match?: 6283591.8 i/s
String#=~: 2581356.8 i/s - 2.43x slower
Regexp#===: 2482379.7 i/s - 2.53x slower
String#match: 2096984.3 i/s - 3.00x slower
See #59 and #62 for discussions.
String#gsub
vs String#sub
vs String#[]=
code
$ ruby -v code/string/gsub-vs-sub.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux]
Warming up --------------------------------------
String#gsub 48.360k i/100ms
String#sub 45.739k i/100ms
String#dup["string"]= 59.896k i/100ms
Calculating -------------------------------------
String#gsub 647.666k (± 3.3%) i/s - 3.240M in 5.008504s
String#sub 756.665k (± 2.0%) i/s - 3.796M in 5.019235s
String#dup["string"]= 917.873k (± 1.8%) i/s - 4.612M in 5.026253s
Comparison:
String#dup["string"]=: 917873.1 i/s
String#sub: 756664.7 i/s - 1.21x slower
String#gsub: 647665.6 i/s - 1.42x slower
String#gsub
vs String#tr
code
$ ruby -v code/string/gsub-vs-tr.rb
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
Calculating -------------------------------------
String#gsub 38.268k i/100ms
String#tr 83.210k i/100ms
-------------------------------------------------
String#gsub 516.604k (± 4.4%) i/s - 2.602M
String#tr 1.862M (± 4.0%) i/s - 9.320M
Comparison:
String#tr: 1861860.4 i/s
String#gsub: 516604.2 i/s - 3.60x slower
Mutable
vs Immutable
code
$ ruby -v code/string/mutable_vs_immutable_strings.rb
ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin14]
Calculating -------------------------------------
Without Freeze 7.279M (± 6.6%) i/s - 36.451M in 5.029785s
With Freeze 9.329M (± 7.9%) i/s - 46.370M in 5.001345s
Comparison:
With Freeze: 9329054.3 i/s
Without Freeze: 7279203.1 i/s - 1.28x slower
String#sub!
vs String#gsub!
vs String#[]=
code
Note that String#[]
will throw an IndexError
when given string or regexp not matched.
$ ruby -v code/string/sub\!-vs-gsub\!-vs-\[\]\=.rb
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14]
Calculating -------------------------------------
String#['string']= 74.512k i/100ms
String#sub!'string' 52.801k i/100ms
String#gsub!'string' 34.480k i/100ms
String#[/regexp/]= 55.325k i/100ms
String#sub!/regexp/ 45.770k i/100ms
String#gsub!/regexp/ 27.665k i/100ms
-------------------------------------------------
String#['string']= 1.215M (± 6.2%) i/s - 6.110M
String#sub!'string' 752.731k (± 6.2%) i/s - 3.749M
String#gsub!'string' 481.183k (± 4.4%) i/s - 2.414M
String#[/regexp/]= 840.615k (± 5.3%) i/s - 4.205M
String#sub!/regexp/ 663.075k (± 7.8%) i/s - 3.295M
String#gsub!/regexp/ 342.004k (± 7.5%) i/s - 1.715M
Comparison:
String#['string']=: 1214845.5 i/s
String#[/regexp/]=: 840615.2 i/s - 1.45x slower
String#sub!'string': 752731.4 i/s - 1.61x slower
String#sub!/regexp/: 663075.3 i/s - 1.83x slower
String#gsub!'string': 481183.5 i/s - 2.52x slower
String#gsub!/regexp/: 342003.8 i/s - 3.55x slower
String#sub
vs String#delete_prefix
code
Ruby 2.5 introduced String#delete_prefix
. Note that this can only be used for removing characters from the start of a string.
$ ruby -v code/string/sub-vs-delete_prefix.rb
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-darwin17]
Calculating -------------------------------------
String#delete_prefix 4.112M (± 1.8%) i/s - 20.707M in 5.037928s
String#sub 814.725k (± 1.4%) i/s - 4.088M in 5.018962s
Comparison:
String#delete_prefix: 4111531.1 i/s
String#sub: 814725.3 i/s - 5.05x slower
String#sub
vs String#chomp
vs String#delete_suffix
code
Ruby 2.5 introduced String#delete_suffix
as a counterpart to delete_prefix
. The performance gain over chomp
is small and during some runs the difference falls within the error margin. Note that this can only be used for removing characters from the end of a string.
$ ruby -v code/string/sub-vs-chomp-vs-delete_suffix.rb
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-darwin17]
Calculating -------------------------------------
String#sub 838.415k (± 1.7%) i/s - 4.214M in 5.027412s
String#chomp 3.951M (± 2.1%) i/s - 19.813M in 5.017089s
String#delete_suffix 4.202M (± 2.1%) i/s - 21.075M in 5.017429s
Comparison:
String#delete_suffix: 4202201.7 i/s
String#chomp: 3950921.9 i/s - 1.06x slower
String#sub: 838415.3 i/s - 5.01x slower
String#unpack1
vs String#unpack[0]
code
Ruby 2.4.0 introduced unpack1
to skip creating the intermediate array object.
$ ruby -v code/string/unpack1-vs-unpack\[0\].rb
ruby 2.4.3p205 (2017-12-14 revision 61247) [x86_64-darwin17]
Warming up --------------------------------------
String#unpack1 224.291k i/100ms
String#unpack[0] 201.870k i/100ms
Calculating -------------------------------------
String#unpack1 4.864M (± 4.2%) i/s - 24.448M in 5.035203s
String#unpack[0] 3.778M (± 4.0%) i/s - 18.976M in 5.031253s
Comparison:
String#unpack1: 4864467.2 i/s
String#unpack[0]: 3777815.6 i/s - 1.29x slower
Remove extra spaces (or other contiguous characters) code
The code is tested against contiguous spaces but should work for other chars too.
$ ruby -v code/string/remove-extra-spaces-or-other-chars.rb
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux]
Warming up --------------------------------------
String#gsub/regex+/ 1.644k i/100ms
String#squeeze 24.681k i/100ms
Calculating -------------------------------------
String#gsub/regex+/ 14.668k (± 5.1%) i/s - 73.980k in 5.056887s
String#squeeze 372.910k (± 8.4%) i/s - 1.851M in 5.011881s
Comparison:
String#squeeze: 372910.3 i/s
String#gsub/regex+/: 14668.1 i/s - 25.42x slower
Time.iso8601
vs Time.parse
code
When expecting well-formatted data from e.g. an API, iso8601
is faster and will raise an ArgumentError
on malformed input.
$ ruby -v code/time/iso8601-vs-parse.rb
ruby 2.4.3p205 (2017-12-14 revision 61247) [x86_64-darwin17]
Warming up --------------------------------------
Time.iso8601 10.234k i/100ms
Time.parse 4.228k i/100ms
Calculating -------------------------------------
Time.iso8601 114.485k (± 3.5%) i/s - 573.104k in 5.012008s
Time.parse 43.711k (± 4.1%) i/s - 219.856k in 5.038349s
Comparison:
Time.iso8601: 114485.1 i/s
Time.parse: 43710.9 i/s - 2.62x slower
cover?
vs include?
code
cover?
only check if it is within the start and end, include?
needs to traverse the whole range.
$ ruby -v code/range/cover-vs-include.rb
ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-linux]
Calculating -------------------------------------
range#cover? 85.467k i/100ms
range#include? 7.720k i/100ms
range#member? 7.783k i/100ms
plain compare 102.189k i/100ms
-------------------------------------------------
range#cover? 1.816M (± 5.6%) i/s - 9.060M
range#include? 83.344k (± 5.0%) i/s - 416.880k
range#member? 82.654k (± 5.0%) i/s - 412.499k
plain compare 2.581M (± 6.2%) i/s - 12.876M
Comparison:
plain compare: 2581211.8 i/s
range#cover?: 1816038.5 i/s - 1.42x slower
range#include?: 83343.9 i/s - 30.97x slower
range#member?: 82654.1 i/s - 31.23x slower
Please! Edit this README.md then Submit a Awesome Pull Request!
Code example is wrong? :cry: Got better example? :heart_eyes: Excellent!
Please open an issue or Open a Pull Request to fix it.
Thank you in advance! :wink: :beer:
Share this with your #Rubyfriends! <3
Brought to you by @JuanitoFatas
Feel free to talk with me on Twitter! <3
Go faster, off the Rails - Benchmarks for your whole Rails app
Talk by Davy Stevenson @ RubyConf 2014.
Provides Big O notation benchmarking for Ruby.
Talk by Prem Sichanugrist @ Ruby Kaigi 2014.
Make your Rubies go faster with this command line tool.
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
To the extent possible under law, @JuanitoFatas has waived all copyright and related or neighboring rights to "fast-ruby".
This work belongs to the community.
Author: fastruby
Source code: https://github.com/fastruby/fast-ruby
License:
1659288120
master
branch) is in maintenance mode! :construction: :warning:We'll gladly accept bugfixes and security-related fixes for v2 (the master
branch), but at this stage, contributions for new features/improvements are welcome only for v3. Please feel free to leave comments in the v3 Pull Request.
A fast JSON:API serializer for Ruby Objects.
Previously this project was called fast_jsonapi, we forked the project and renamed it to jsonapi/serializer in order to keep it alive.
We would like to thank the Netflix team for the initial work and to all our contributors and users for the continuous support!
Performance Comparison
We compare serialization times with ActiveModelSerializer
and alternative implementations as part of performance tests available at jsonapi-serializer/comparisons.
We want to ensure that with every change on this library, serialization time stays significantly faster than the performance provided by the alternatives. Please read the performance article in the docs
folder for any questions related to methodology.
Table of Contents
belongs_to
, has_many
and has_one
Add this line to your application's Gemfile:
gem 'jsonapi-serializer'
Execute:
$ bundle install
You can use the bundled generator if you are using the library inside of a Rails project:
rails g serializer Movie name year
This will create a new serializer in app/serializers/movie_serializer.rb
class Movie
attr_accessor :id, :name, :year, :actor_ids, :owner_id, :movie_type_id
end
class MovieSerializer
include JSONAPI::Serializer
set_type :movie # optional
set_id :owner_id # optional
attributes :name, :year
has_many :actors
belongs_to :owner, record_type: :user
belongs_to :movie_type
end
movie = Movie.new
movie.id = 232
movie.name = 'test movie'
movie.actor_ids = [1, 2, 3]
movie.owner_id = 3
movie.movie_type_id = 1
movie
movies =
2.times.map do |i|
m = Movie.new
m.id = i + 1
m.name = "test movie #{i}"
m.actor_ids = [1, 2, 3]
m.owner_id = 3
m.movie_type_id = 1
m
end
hash = MovieSerializer.new(movie).serializable_hash
json_string = MovieSerializer.new(movie).serializable_hash.to_json
{
"data": {
"id": "3",
"type": "movie",
"attributes": {
"name": "test movie",
"year": null
},
"relationships": {
"actors": {
"data": [
{
"id": "1",
"type": "actor"
},
{
"id": "2",
"type": "actor"
}
]
},
"owner": {
"data": {
"id": "3",
"type": "user"
}
}
}
}
}
set_type
By default fast_jsonapi will try to figure the type based on the name of the serializer class. For example class MovieSerializer
will automatically have a type of :movie
. If your serializer class name does not follow this format, you have to manually state the set_type
at the serializer.
By default fast_jsonapi underscores the key names. It supports the same key transforms that are supported by AMS. Here is the syntax of specifying a key transform
class MovieSerializer
include JSONAPI::Serializer
# Available options :camel, :camel_lower, :dash, :underscore(default)
set_key_transform :camel
end
Here are examples of how these options transform the keys
set_key_transform :camel # "some_key" => "SomeKey"
set_key_transform :camel_lower # "some_key" => "someKey"
set_key_transform :dash # "some_key" => "some-key"
set_key_transform :underscore # "some_key" => "some_key"
Attributes are defined using the attributes
method. This method is also aliased as attribute
, which is useful when defining a single attribute.
By default, attributes are read directly from the model property of the same name. In this example, name
is expected to be a property of the object being serialized:
class MovieSerializer
include JSONAPI::Serializer
attribute :name
end
Custom attributes that must be serialized but do not exist on the model can be declared using Ruby block syntax:
class MovieSerializer
include JSONAPI::Serializer
attributes :name, :year
attribute :name_with_year do |object|
"#{object.name} (#{object.year})"
end
end
The block syntax can also be used to override the property on the object:
class MovieSerializer
include JSONAPI::Serializer
attribute :name do |object|
"#{object.name} Part 2"
end
end
Attributes can also use a different name by passing the original method or accessor with a proc shortcut:
class MovieSerializer
include JSONAPI::Serializer
attributes :name
attribute :released_in_year, &:year
end
Links are defined using the link
method. By default, links are read directly from the model property of the same name. In this example, public_url
is expected to be a property of the object being serialized.
You can configure the method to use on the object for example a link with key self
will get set to the value returned by a method called url
on the movie object.
You can also use a block to define a url as shown in custom_url
. You can access params in these blocks as well as shown in personalized_url
class MovieSerializer
include JSONAPI::Serializer
link :public_url
link :self, :url
link :custom_url do |object|
"https://movies.com/#{object.name}-(#{object.year})"
end
link :personalized_url do |object, params|
"https://movies.com/#{object.name}-#{params[:user].reference_code}"
end
end
You can specify relationship links by using the links:
option on the serializer. Relationship links in JSON API are useful if you want to load a parent document and then load associated documents later due to size constraints (see related resource links)
class MovieSerializer
include JSONAPI::Serializer
has_many :actors, links: {
self: :url,
related: -> (object) {
"https://movies.com/#{object.id}/actors"
}
}
end
Relationship links can also be configured to be defined as a method on the object.
has_many :actors, links: :actor_relationship_links
This will create a self
reference for the relationship, and a related
link for loading the actors relationship later. NB: This will not automatically disable loading the data in the relationship, you'll need to do that using the lazy_load_data
option:
has_many :actors, lazy_load_data: true, links: {
self: :url,
related: -> (object) {
"https://movies.com/#{object.id}/actors"
}
}
For every resource in the collection, you can include a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship.
class MovieSerializer
include JSONAPI::Serializer
meta do |movie|
{
years_since_release: Date.current.year - movie.year
}
end
end
You can specify relationship meta by using the meta:
option on the serializer. Relationship meta in JSON API is useful if you wish to provide non-standard meta-information about the relationship.
Meta can be defined either by passing a static hash or by using Proc to the meta
key. In the latter case, the record and any params passed to the serializer are available inside the Proc as the first and second parameters, respectively.
class MovieSerializer
include JSONAPI::Serializer
has_many :actors, meta: Proc.new do |movie_record, params|
{ count: movie_record.actors.length }
end
end
Support for top-level and nested included associations through options[:include]
.
options = {}
options[:meta] = { total: 2 }
options[:links] = {
self: '...',
next: '...',
prev: '...'
}
options[:include] = [:actors, :'actors.agency', :'actors.agency.state']
MovieSerializer.new(movies, options).serializable_hash.to_json
options[:meta] = { total: 2 }
options[:links] = {
self: '...',
next: '...',
prev: '...'
}
hash = MovieSerializer.new(movies, options).serializable_hash
json_string = MovieSerializer.new(movies, options).serializable_hash.to_json
You can use is_collection
option to have better control over collection serialization.
If this option is not provided or nil
autodetect logic is used to try understand if provided resource is a single object or collection.
Autodetect logic is compatible with most DB toolkits (ActiveRecord, Sequel, etc.) but cannot guarantee that single vs collection will be always detected properly.
options[:is_collection]
was introduced to be able to have precise control this behavior
nil
or not provided: will try to autodetect single vs collection (please, see notes above)true
will always treat input resource as collectionfalse
will always treat input resource as single objectTo enable caching, use cache_options store: <cache_store>
:
class MovieSerializer
include JSONAPI::Serializer
# use rails cache with a separate namespace and fixed expiry
cache_options store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 1.hour
end
store
is required can be anything that implements a #fetch(record, **options, &block)
method:
record
is the record that is currently serializedoptions
is everything that was passed to cache_options
except store
, so it can be everyhing the cache store supports&block
should be executed to fetch new data if cache is emptySo for the example above it will call the cache instance like this:
Rails.cache.fetch(record, namespace: 'jsonapi-serializer', expires_in: 1.hour) { ... }
If caching is enabled and fields are provided to the serializer, the fieldset will be appended to the cache key's namespace.
For example, given the following serializer definition and instance:
class ActorSerializer
include JSONAPI::Serializer
attributes :first_name, :last_name
cache_options store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 1.hour
end
serializer = ActorSerializer.new(actor, { fields: { actor: [:first_name] } })
The following cache namespace will be generated: 'jsonapi-serializer-fieldset:first_name'
.
In some cases, attribute values might require more information than what is available on the record, for example, access privileges or other information related to a current authenticated user. The options[:params]
value covers these cases by allowing you to pass in a hash of additional parameters necessary for your use case.
Leveraging the new params is easy, when you define a custom id, attribute or relationship with a block you opt-in to using params by adding it as a block parameter.
class MovieSerializer
include JSONAPI::Serializer
set_id do |movie, params|
# in here, params is a hash containing the `:admin` key
params[:admin] ? movie.owner_id : "movie-#{movie.id}"
end
attributes :name, :year
attribute :can_view_early do |movie, params|
# in here, params is a hash containing the `:current_user` key
params[:current_user].is_employee? ? true : false
end
belongs_to :primary_agent do |movie, params|
# in here, params is a hash containing the `:current_user` key
params[:current_user]
end
end
# ...
current_user = User.find(cookies[:current_user_id])
serializer = MovieSerializer.new(movie, {params: {current_user: current_user}})
serializer.serializable_hash
Custom attributes and relationships that only receive the resource are still possible by defining the block to only receive one argument.
Conditional attributes can be defined by passing a Proc to the if
key on the attribute
method. Return true
if the attribute should be serialized, and false
if not. The record and any params passed to the serializer are available inside the Proc as the first and second parameters, respectively.
class MovieSerializer
include JSONAPI::Serializer
attributes :name, :year
attribute :release_year, if: Proc.new { |record|
# Release year will only be serialized if it's greater than 1990
record.release_year > 1990
}
attribute :director, if: Proc.new { |record, params|
# The director will be serialized only if the :admin key of params is true
params && params[:admin] == true
}
# Custom attribute `name_year` will only be serialized if both `name` and `year` fields are present
attribute :name_year, if: Proc.new { |record|
record.name.present? && record.year.present?
} do |object|
"#{object.name} - #{object.year}"
end
end
# ...
current_user = User.find(cookies[:current_user_id])
serializer = MovieSerializer.new(movie, { params: { admin: current_user.admin? }})
serializer.serializable_hash
Conditional relationships can be defined by passing a Proc to the if
key. Return true
if the relationship should be serialized, and false
if not. The record and any params passed to the serializer are available inside the Proc as the first and second parameters, respectively.
class MovieSerializer
include JSONAPI::Serializer
# Actors will only be serialized if the record has any associated actors
has_many :actors, if: Proc.new { |record| record.actors.any? }
# Owner will only be serialized if the :admin key of params is true
belongs_to :owner, if: Proc.new { |record, params| params && params[:admin] == true }
end
# ...
current_user = User.find(cookies[:current_user_id])
serializer = MovieSerializer.new(movie, { params: { admin: current_user.admin? }})
serializer.serializable_hash
In many cases, the relationship can automatically detect the serializer to use.
class MovieSerializer
include JSONAPI::Serializer
# resolves to StudioSerializer
belongs_to :studio
# resolves to ActorSerializer
has_many :actors
end
At other times, such as when a property name differs from the class name, you may need to explicitly state the serializer to use. You can do so by specifying a different symbol or the serializer class itself (which is the recommended usage):
class MovieSerializer
include JSONAPI::Serializer
# resolves to MovieStudioSerializer
belongs_to :studio, serializer: :movie_studio
# resolves to PerformerSerializer
has_many :actors, serializer: PerformerSerializer
end
For more advanced cases, such as polymorphic relationships and Single Table Inheritance, you may need even greater control to select the serializer based on the specific object or some specified serialization parameters. You can do by defining the serializer as a Proc
:
class MovieSerializer
include JSONAPI::Serializer
has_many :actors, serializer: Proc.new do |record, params|
if record.comedian?
ComedianSerializer
elsif params[:use_drama_serializer]
DramaSerializer
else
ActorSerializer
end
end
end
has_many
RelationshipYou can order the has_many
relationship by providing a block:
class MovieSerializer
include JSONAPI::Serializer
has_many :actors do |movie|
movie.actors.order(position: :asc)
end
end
Attributes and relationships can be selectively returned per record type by using the fields
option.
class MovieSerializer
include JSONAPI::Serializer
attributes :name, :year
end
serializer = MovieSerializer.new(movie, { fields: { movie: [:name] } })
serializer.serializable_hash
You can mix-in code from another ruby module into your serializer class to reuse functions across your app.
Since a serializer is evaluated in a the context of a class
rather than an instance
of a class, you need to make sure that your methods act as class
methods when mixed in.
Using ActiveSupport::Concern
module AvatarHelper
extend ActiveSupport::Concern
class_methods do
def avatar_url(user)
user.image.url
end
end
end
class UserSerializer
include JSONAPI::Serializer
include AvatarHelper # mixes in your helper method as class method
set_type :user
attributes :name, :email
attribute :avatar do |user|
avatar_url(user)
end
end
Using Plain Old Ruby
module AvatarHelper
def avatar_url(user)
user.image.url
end
end
class UserSerializer
include JSONAPI::Serializer
extend AvatarHelper # mixes in your helper method as class method
set_type :user
attributes :name, :email
attribute :avatar do |user|
avatar_url(user)
end
end
Option | Purpose | Example |
---|---|---|
set_type | Type name of Object | set_type :movie |
key | Key of Object | belongs_to :owner, key: :user |
set_id | ID of Object | set_id :owner_id or set_id { |record, params| params[:admin] ? record.id : "#{record.name.downcase}-#{record.id}" } |
cache_options | Hash with store to enable caching and optional further cache options | cache_options store: ActiveSupport::Cache::MemoryStore.new, expires_in: 5.minutes |
id_method_name | Set custom method name to get ID of an object (If block is provided for the relationship, id_method_name is invoked on the return value of the block instead of the resource object) | has_many :locations, id_method_name: :place_ids |
object_method_name | Set custom method name to get related objects | has_many :locations, object_method_name: :places |
record_type | Set custom Object Type for a relationship | belongs_to :owner, record_type: :user |
serializer | Set custom Serializer for a relationship | has_many :actors, serializer: :custom_actor , has_many :actors, serializer: MyApp::Api::V1::ActorSerializer , or has_many :actors, serializer -> (object, params) { (return a serializer class) } |
polymorphic | Allows different record types for a polymorphic association | has_many :targets, polymorphic: true |
polymorphic | Sets custom record types for each object class in a polymorphic association | has_many :targets, polymorphic: { Person => :person, Group => :group } |
Performance instrumentation is available by using the active_support/notifications
.
To enable it, include the module in your serializer class:
require 'jsonapi/serializer'
require 'jsonapi/serializer/instrumentation'
class MovieSerializer
include JSONAPI::Serializer
include JSONAPI::Serializer::Instrumentation
# ...
end
Skylight integration is also available and supported by us, follow the Skylight documentation to enable it.
The project has and requires unit tests, functional tests and performance tests. To run tests use the following command:
rspec
We currently do not support deserialization, but we recommend to use any of the next gems:
This gem provides the next features alongside deserialization:
If you come from Netflix/fast_jsonapi, here is the instructions to switch.
- gem 'fast_jsonapi'
+ gem 'jsonapi-serializer'
class MovieSerializer
- include FastJsonapi::ObjectSerializer
+ include JSONAPI::Serializer
end
- json_string = MovieSerializer.new(movie).serialized_json
+ json_string = MovieSerializer.new(movie).serializable_hash.to_json
- require 'fast_jsonapi'
+ require 'jsonapi/serializer'
See docs.
- cache_options enabled: true, cache_length: 12.hours
+ cache_options store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 1.hour
Please follow the instructions we provide as part of the issue and pull request creation processes.
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
Author: jsonapi-serializer
Source code: https://github.com/jsonapi-serializer/jsonapi-serializer
License: Apache-2.0 license