Recent Organization Move
Please change your local remote to pull from this repository:
git remote set-url [previous-remote-name] [email protected]:procore-oss/blueprinter.git
to see the previous upstream remote name, run:
git remote -v
Blueprinter
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.
Documentation
Docs can be found here.
Usage
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: ```ruby class UserBlueprint < Blueprinter::Base identifier :uuid fields :first_name, :last_name, :email end ``` and then, in your code: ```ruby puts UserBlueprint.render(user) # Output is a JSON string ``` And the output would look like: ```json { "uuid": "733f0758-8f21-4719-875f-262c3ec743af", "email": "[email protected]", "first_name": "John", "last_name": "Doe" } ```Collections
You can also pass a collection object or an array to the render method. ```ruby puts UserBlueprint.render(User.all) ``` This will result in JSON that looks something like this: ```json [ { "uuid": "733f0758-8f21-4719-875f-262c3ec743af", "email": "[email protected]", "first_name": "John", "last_name": "Doe" }, { "uuid": "733f0758-8f21-4719-875f-743af262c3ec", "email": "[email protected]", "first_name": "John", "last_name": "Doe 2" } ] ``` You can also configure other classes to be treated like collections. For example, if you are using Mongoid, you can configure it to treat `Mongoid::Criteria` objects as collections: ```ruby Blueprinter.configure do |config| config.custom_array_like_classes = [Mongoid::Criteria] end ``` Or if you wanted it to treat the `Set` class as a collection: ```ruby Blueprinter.configure do |config| config.custom_array_like_classes = [Set] end ```Renaming
You can rename the resulting JSON keys in both fields and associations by using the `name` option. ```ruby 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: ```json { "uuid": "92a5c732-2874-41e4-98fc-4123cd6cfa86", "login": "[email protected]", "projects": [] } ```Views
You may define different outputs by utilizing views: ```ruby 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: ```ruby puts UserBlueprint.render(user, view: :extended) ``` Output: ```json { "uuid": "733f0758-8f21-4719-875f-262c3ec743af", "address": "123 Fake St.", "first_name": "John", "last_name": "Doe", "login": "[email protected]" } ```Identifiers
`identifier`s are used to specify a field or method name used as an identifier. Usually, this is something like `:id`. Example: ```rb class UserBlueprint < Blueprinter::Base identifier :uuid end ``` Blueprinter `identifier`s have a few properties that set them apart from `field`s. 1. Identifiers are **always** rendered and considered their own view (the `:identifier` view). 2. When rendering, identifier fields are always sorted first, before other fields. 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: ```ruby class UserBlueprint < Blueprinter::Base identifier :uuid field :email, name: :login view :normal do fields :first_name, :last_name end end ``` Usage: ```ruby puts UserBlueprint.render(user, view: :normal, root: :user) ``` Output: ```json { "user": { "uuid": "733f0758-8f21-4719-875f-262c3ec743af", "first_name": "John", "last_name": "Doe", "login": "[email protected]" } } ```Meta Attributes
You can additionally add meta-data to the json as well: ```ruby class UserBlueprint < Blueprinter::Base identifier :uuid field :email, name: :login view :normal do fields :first_name, :last_name end end ``` Usage: ```ruby json = UserBlueprint.render(user, view: :normal, root: :user, meta: [ 'https://app.mydomain.com', 'https://alternate.mydomain.com' ]) puts json ``` Output: ```json { "user": { "uuid": "733f0758-8f21-4719-875f-262c3ec743af", "first_name": "John", "last_name": "Doe", "login": "[email protected]" }, "meta": { "links": [ "https://app.mydomain.com", "https://alternate.mydomain.com" ] } } ``` _NOTE:_ For meta attributes, a [root](#root) is mandatory.Exclude Fields
You can specifically choose to exclude certain fields for specific views ```ruby 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: ```ruby puts UserBlueprint.render(user, view: :extended) ``` Output: ```json { "uuid": "733f0758-8f21-4719-875f-262c3ec743af", "address": "123 Fake St.", "first_name": "John", "login": "[email protected]" } ``` Use `excludes` to exclude multiple fields at once inline. ```ruby 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: ```ruby 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: ```ruby puts UserBlueprint.render(user, view: :normal) ``` Output: ```json { "uuid": "733f0758-8f21-4719-875f-262c3ec743af", "first_name": "John", "last_name": "Doe", "login": "[email protected]", "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: ```ruby class VehicleBlueprint < Blueprinter::Base identifier :uuid field :full_name do |vehicle, options| "#vehiclevehicle.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. ### Global Config Setting ```ruby Blueprinter.configure do |config| config.field_default = "N/A" config.association_default = {} end ``` ### Field-level/Association-level Setting ```ruby 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). ### Blueprinter::EMPTY_COLLECTION An empty array or empty active record collection. ### Blueprinter::EMPTY_HASH An empty hash. ### Blueprinter::EMPTY_STRING An empty string or symbol. #### Field-level/Association-level Setting - EMPTY_STRING ```ruby 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::EMPTY_STRING, default: "N/A" # If the projects association collection is empty, it will become nil association :projects, blueprint: ProjectBlueprint, default_if: Blueprinter::EMPTY_COLLECTION 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. ```ruby 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) taskabletaskable.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: ```ruby class UserBlueprint < Blueprinter::Base identifier :uuid field :full_name do |user, options| "#options[:title_prefix] #useruser.first_name #useruser.last_name" end end ``` Usage: ```ruby puts UserBlueprint.render(user, title_prefix: "Mr") ``` Output: ```json { "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: ```ruby class UserBlueprint < Blueprinter::Base identifier :uuid do |user, options| options[:current_user].anonymize(user.uuid) end end ``` Usage: ```ruby puts UserBlueprint.render(user, current_user: current_user) ``` Output: ```json { "uuid": "733f0758-8f21-4719-875f-262c3ec743af", } ```Defining An Association Directly In The Blueprint
You can also pass a block to an association: ```ruby 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: ```ruby puts UserBlueprint.render(user, draft_projects: Project.where(draft: true)) ``` Output: ```json { "uuid": "733f0758-8f21-4719-875f-262c3ec743af", "projects": [ "b426a1e6-ac41-45ab-bfef-970b9a0b4289", "name": "query-console", "5bd84d6c-4fd2-4e36-ae31-c137e39be542", "name": "blueprinter", "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: ```ruby class UserBlueprint < Blueprinter::Base identifier :uuid field(:company_name) do |_user, options| options[:company].name end end ``` Usage: ```ruby puts UserBlueprint.render(user, company: company) ``` Output: ```json { "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. ### Global Config Setting - if and unless ```ruby 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 ``` #### Field-level Setting ```ruby 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.Exclude Fields with nil Values
By default, fields with `nil` values are included when rendering. You can override this behavior by setting `:exclude_if_nil: true` in the field definition. Usage: ```ruby class UserBlueprint < Blueprinter::Base identifier :uuid field :name field :birthday, exclude_if_nil: true end user = User.new(name: 'John Doe') puts UserBlueprint.render(user) ``` Output: ```json { "name": "John Doe" } ```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. #### Global Config Setting - datetime 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`. ```ruby Blueprinter.configure do |config| config.datetime_format = ->(datetime) { datetime.nil? ? datetime : datetime.strftime("%s").to_i } end ``` #### Field-level Setting - datetime_format Usage (String Option): ```ruby class UserBlueprint < Blueprinter::Base identifier :name field :birthday, datetime_format: "%m/%d/%Y" end ``` Output: ```json { "name": "John Doe", "birthday": "03/04/1994" } ``` Usage (Proc Option): ```ruby class UserBlueprint < Blueprinter::Base identifier :name field :birthday, datetime_format: ->(datetime) { datetime.nil? ? datetime : datetime.strftime("%s").to_i } end ``` Output: ```json { "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. The modified `hash` object will be the resulting hash passed to serialization. #### Example Create a Transform class extending from `Blueprinter::Transformer` ```ruby class DynamicFieldTransformer < Blueprinter::Transformer def transform(hash, object, _options) hash.merge!(object.dynamic_fields) end end ``` ```ruby class User def dynamic_fields case role when :admin { employer: employer, time_in_role: determine_time_in role } when :maintainer { label: label, settings: generate_settings_hash } when :read_only { last_login_at: last_login_at } end end end ``` Then specify the transform to use for the view. ```ruby class UserBlueprint < Blueprinter::Base fields :first_name, :last_name transform DynamicTransformer end ``` #### Transform across views Transformers can be included across views: ```ruby class UserBlueprint < Blueprinter::Base transform DefaultTransformer view :normal do transform ViewTransformer end view :extended do include_view :normal end end ``` Both the `normal` and `extended` views have `DefaultTransformer` and `ViewTransformer` applied. Transformers are executed in a top-down order, so `DefaultTransformer` will be executed first, followed by `ViewTransformer`. #### Global Transforms You can also specify global default transformers. Create one or more transformer classes extending from `Blueprinter::Transformer` and set the `default_transformers` configuration ```ruby class LowerCamelTransformer < Blueprinter::Transformer def transform(hash, _object, _options) hash.transform_keys! { |key| key.to_s.camelize(:lower).to_sym } end end ``` ```ruby 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. #### Examples For a specific kind of field, create an extractor class extending from `Blueprinter::Extractor` ```ruby class MyFieldExtractor < Blueprinter::Extractor def extract(_field_name, _object, _local_options, _options={}) # process your obscure_object _object.clarified end end ``` ```ruby 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 ```ruby 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 ``` ```ruby 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: ```ruby Blueprinter.configure do |config| config.sort_fields_by = :definition end ``` ```ruby class UserBlueprint < Blueprinter::Base identifier :name field :email field :birthday, datetime_format: "%m/%d/%Y" end ``` Output: ```json { "name": "John Doe", "email": "[email protected]", "birthday": "03/04/1994" } ```Reflection
Blueprint classes may be reflected on to inspect their views, fields, and associations. Extensions often make use of this ability. ```ruby class WidgetBlueprint < Blueprinter::Base fields :name, :description association :category, blueprint: CategoryBlueprint view :extended do field :price association :parts, blueprint: WidgetPartBlueprint end end # A Hash of views keyed by name views = WidgetBlueprint.reflections views.keys => [:default, :extended] # Hashes of fields and associations, keyed by name fields = views[:default].fields assoc = views[:default].associations # Get info about a field fields[:description].name fields[:description].display_name fields[:description].options # Get info about an association assoc[:category].name assoc[:category].display_name assoc[:category].blueprint assoc[:category].view assoc[:category].options ```Extensions
Blueprinter offers an extension system to hook into and modify certain behavior. ```ruby Blueprinter.configure do |config| config.extensions << MyExtension.new config.extensions << OtherExtension.new end ``` Extension hooks: * [pre_render](https://github.com/procore-oss/blueprinter/blob/abca9ca8ed23edd65a0f4b5ae43e25b8e27a2afc/lib/blueprinter/extension.rb#L18): Intercept the object before rendering begins Some known extensions are: * [blueprinter-activerecord](https://github.com/procore-oss/blueprinter-activerecord)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 | ### Example - deprecations ```ruby Blueprinter.configure do |config| config.deprecations = :raise end ```render_as_hash
Same as `render`, returns a Ruby Hash. Usage: ```ruby puts UserBlueprint.render_as_hash(user, company: company) ``` Output: ```ruby { 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: ```ruby puts UserBlueprint.render_as_json(user, company: company) ``` Output: ```ruby { "uuid" => "733f0758-8f21-4719-875f-262c3ec743af", "company_name" => "My Company LLC" } ```Installation
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.
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
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.
Contributing
Please read our Contributing file
Tests
You can run tests with bundle exec rake
.
Maintain The Docs
We use Yard for documentation. Here are the following documentation rules:
- Document all public methods we expect to be utilized by the end developers.
- Methods that are not set to private due to ruby visibility rule limitations should be marked with
@api private
.
How to Document
We use Yard for documentation. Here are the following documentation rules:
- Document all public methods we expect to be utilized by the end developers.
- Methods that are not set to private due to ruby visibility rule limitations should be marked with
@api private
.
Releasing a New Version
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.
License
The gem is available as open source under the terms of the MIT License.