Blueprinter
Warning
This is a WIP for API V2!
Blueprinter is a JSON serializer for your business objects. It is designed to be simple, flexible, and performant.
Upgrading from Blueprinter V1? Read over the changes!
Installation
bundle add blueprinter
See rubydoc.info/gems/blueprinter for API documentation.
Basic Usage
class WidgetBlueprint < ApplicationBlueprint
field :name
association :category, CategoryBlueprint
association :parts, [PartBlueprint]
view :extended do
field :description
association :manufacturer, CompanyBlueprint
association :vendors, [CompanyBlueprint]
end
end
# Render the default view to JSON
WidgetBlueprint.render(widget).to_json
# Render the extended view to a Hash
WidgetBlueprint[:extended].render(widget).to_hash
Changes in V2
While Blueprinter V2 is recognizably Blueprinter, there are several breaking changes. Some of them will require code changes while others can be made backwards-compatible using bundled extensions.
# Can you spot the differences?
class WidgetBlueprint < ApplicationBlueprint
field :name
field(:desc) { |widget| widget.description[0..100] }
association :category, CategoryBlueprint
view :extended do
field :desc, source: :description
association :subcategory, CategoryBlueprint[:subcategory]
association :parts, [PartBlueprint]
end
end
Interoperability
The initial release of V2 is included alongside legacy/V1 code, allowing you to migrate your codebase slowly. V2 Blueprints can reference legacy/V1 Blueprints in associations, and vice versa.
Eventually the legacy/V1 code will be removed, necessitating a complete switch to V2.
Breaking Changes
When you migrate a Blueprint to V2, you’ll be required to make the changes in this section.
Generally speaking the Blueprint DSL is easy to upgrade. But if you’re using extensive global configuration options, be prepared to change how they work.
Breaking Rendering Changes
If you’re using Rails’ render json: you don’t have to change anything:
render json: WidgetBlueprint.render(widget)
Otherwise, it now looks like this:
# JSON string
WidgetBlueprint.render(widget).to_json
# Hash (or array of Hashes if you passed an Enumerable)
WidgetBlueprint.render(widget).to_hash
Note
The legacy
render_as_hashandrender_as_jsonmethods are still present but may be removed in a future release.
Breaking DSL Changes
At a quick glance the DSL looks nearly identical, but don’t be fooled! See the docs of Blueprinter::V2::DSL for complete documentation of all new features.
Association syntax
Associations in V2 are more concise while conveying more information.
# A single object
association :category, CategoryBlueprint
association :category, CategoryBlueprint[:my_view]
# A collection (Enumerable) of objects
association :parts, [PartBlueprint]
association :parts, [PartBlueprint[:my_view]]
To dynamically reference the current view’s name, use view_name or view_path:
view :extended do
# ...
view :plus do
# Looks for a view named 'plus'
association :category, CategoryBlueprint[view_name]
# Looks for a nested view named 'extended.plus'
association :category, CategoryBlueprint[view_path]
end
end
Learn more about nested views.
Second arg in field blocks
If you’re using the second argument in your field/association blocks, it’s no longer the render options. It’s now a Blueprinter::V2::Context::Field
object, which contains the render options along with a lot more.
You can update your block in a single line:
field :description do |object, ctx|
options = ctx.options
# ...
end
Including a view
If you’re including a view in another view, replace include_view with use.
view :my_view do
use :other_view
# ...
end
This change may look superfluous, but it’s the result of a cool new feature: partials.
Transformers
Transforming a Blueprint’s output can be done using the around_blueprint extension hook. (See Blueprinter::Extension for full Extension API docs).
However, the LegacyTransformer extension offers compatibility with legacy/V1 transformer classes:
add Blueprinter::Extensions::LegacyTransformer.new(MyTransformer)
identifier
You can easily replicate legacy/V1’s identifier field and view in your base Blueprint:
class ApplicationBlueprint < Blueprinter::Extension
# The id field will be added to all Blueprints and views
field :id
# All Blueprints will get an identifier view, and it will only ever contain id
view :identifier do
exclude fields: true
field :id
end
end
Breaking Configuration Changes
Configuration in V2 uses a completely different paradigm. There is no global configuration in V2. All configuration happens in Blueprints, views, or partials by setting options or adding extensions.
Options and extensions are inherited from parent Blueprints and views and can be overridden by their children. This allows different parts of your system
to be configured differently. “Global” configuration should be placed in your application’s base Blueprint, e.g. ApplicationBlueprint.
Each legacy/V1 global configuration option is listed below, along with its V2 equivalent.
association_default / field_default
Set the :default option in your base Blueprint. It will be passed a Blueprinter::V2::Context::Field object, containing the current field,
the current object, and a lot more.
set :default, ->(ctx) {
case ctx.field.type
when :field then "N/A"
when :object then {}
when :collection then []
end
}
if / unless
Set the :if and :unless options in your base Blueprint. They will be passed a Blueprinter::V2::Context::Field object, containing the
current field, the current object, and a lot more.
set :if, ->(ctx) {
# extract the V1 args from ctx
field_name = ctx.field.source
object = ctx.object
options = ctx.options
# ...
}
custom_array_like_classes
render has sensible heuristics for detecting what’s a “collection” or not: any Enumerable except for Hash.
If that logic doesn’t work for something, use one of these methods in place of render:
# Force `arg` to be treated like a collection (must respond to `map` with an Enumerable)
array = WidgetBlueprint.render_collection(arg).to_hash
# Force `arg` to be treated like an object
hash = WidgetBlueprint.render_object(arg).to_hash
extractor_default
“Extractors” are not a discrete concept in V2, but they can be implemented using the around_field_value, around_object_value, and around_collection_value
extension hooks.
The bundled LegacyExtractorOption extension can be enabled to offer backwards-compatibility with legacy/V1’s extractors:
# Add the extension
add Blueprinter::Extensions::LegacyExtractorOption.new
# Set the `extractor` option
set :extractor, MyExtractor
datetime_format
Formatting can now be applied to any class. Format dates, times, booleans, or anything else. Define them in your blueprints, views, or partials.
# You can define them with blocks
format(TrueClass) { |val| "Y" }
format(FalseClass) { |val| "N" }
# Or with method names
format Date, :iso8601
format Time, :iso8601
def iso8601(val) = val.iso8601
See the Documentation for Blueprinter::V2::DSL#format.
default_transformers
Transformations can be done using V2’s around_blueprint extension hook (See Blueprinter::Extension for full Extension API docs).
However, the bundled LegacyTransformer extension offers compatibility with legacy/V1 transformer classes:
add Blueprinter::Extensions::LegacyTransformer.new(
MyTransformer, OtherTransformer
)
generator / method
To use a different JSON serializer you can use the Blueprinter::Extensions::MultiJson extension. (You’ll need the multi_json gem installed and
configured).
# Be sure this is the FIRST extension you add
add Blueprinter::Extensions::MultiJson.new
If multi_json doesn’t support your serializer, you can create your own extension using the around_result hook:
class MyJsonExtension < Blueprinter::Extension
def around_result(ctx)
case ctx.format
when :json
result = yield ctx
json = MySerializer.dump result
# The `serialized` helper tells Blueprinter we've already JSONified the result
serialized json
else
yield ctx
end
end
end
class ApplicationBlueprint < Blueprinter::V2::Base
# Be sure this is the FIRST extension you add
add MyJsonExtension.new
end
sort_fields_by
By default V2 serializes fields in the order they were defined. If you want a different order, use the bundled FieldOrder extension or
the around_blueprint_init extension hook.
The following replicates legacy/V1’s default field order of “alphabetical with id first”:
add Blueprinter::Extensions::FieldOrder.new { |a, b|
if a.name == :id
-1
elsif b.name == :id
1
else
a.name <=> b.name
end
}
extensions
Add a “global” extension by adding it to your base Blueprint:
add MyExtension.new
V2 Extension Hook API
V2 has a completely different, and much more poweful, hook system. Be sure your extension supports the V2 API!
Read Breaking Extension Changes, the Extension Guide, or the documentation for Blueprinter::Extension for more info.
Breaking Reflection Changes
Note
Most applications don’t use reflection and can skip this section.
V2’s reflection API is backwards compatible with the exception of renamed fields. This is a result of a change in the DSL.
V1 style
class MyBlueprint < Blueprinter::Base
# A field named "desc" that pulls from "description"
field :description, name: :desc
end
field = MyBlueprint.reflections[:default].fields[:desc]
puts field.display_name # name of serialized field
# => :desc
puts field.name # name of field's source
# => :description
V2 style
class MyBlueprint < Blueprinter::Base
# A field named "desc" that pulls from "description"
field :desc, source: :description
end
field = MyBlueprint.reflections[:default].fields[:desc]
puts field.name # name of serialized field
# => :desc
puts field.source # name of field's source
# => :description
Breaking Extension Changes
Note
Most applications don’t use extensions (yet) and can skip this section.
Legacy/V1 had only one extension hook: pre_render. V2’s closest analog is around_result, which runs once at the beginning of every call to render.
Here’s an example of an extension that supports both V1 and V2:
class MyExtension < Blueprinter::Extension
# V1 API
def pre_render(object, blueprint_class, view, options)
modify(object, blueprint_class, view, options)
end
# V2 API
def around_result(ctx)
blueprint_class = ctx.blueprint.class
view = blueprint_class.view_name
ctx.object = modify(ctx.object, blueprint_class, view, ctx.options)
yield ctx
end
private
def modify(object, blueprint_class, view, options)
# return a modified or new object
end
end
See Blueprinter::Extension for the full V2 extension API, or read the Extension Guide.
Compatible Changes
When you migrate a Blueprint to V2 you can enable the LegacyOptions extension to maintain backwards-compatibility with many V1 options.
class ApplicationBlueprint < Blueprinter::V2::Base
add Blueprinter::Extensions::LegacyOptions.new
end
Note
These extensions don’t replace V2’s native behavior. Instead, they detect and convert V1-style options. This allows your application to migrate to V2 at a gradual pace.
Rendering views
V2’s method of rendering a view is:
WidgetBlueprint[:my_view].render(widget).to_json
However, with the LegacyOptions extension you may continue using V1-style view rendering:
WidgetBlueprint.render(widget, view: :my_view).to_json
If/unless options
V2’s if and unless options have different Proc arguments than legacy/V1. If you enable the LegacyOptions extension, if/unless Procs with three arguments will continue to work like they did in V1:
field :a, if: ->(field_name, object, options) { object.active? }
V2 style
V2-style if/unless Procs will continue to work. They accept a single Blueprinter::V2::Context::Field argument:
field :a, if: ->(ctx) {
field_name = ctx.field.source
object = ctx.object
options = ctx.options
object.active?
}
default_if option
V2 has a much more flexible default_if option, but you can maintain backwards compatibility with V1 using the LegacyOptions extension.
The old default_if values will continue to work (until a future Blueprinter removes the constants).
field :name, default: "N/A", default_if: Blueprinter::EMPTY_STRING
V2 style
The native V2 option allows for Procs or symbols (method names). The logic is up to you.
They’re passed a Blueprinter::V2::Context::Field object and the extracted value.
field :name, default: "N/A", default_if: ->(ctx, val) { val.empty? }
field :name, default: "N/A", default_if: :empty_string?
def empty_string?(ctx, val) = val.empty?
Field name option
In Blueprinter Legacy/V1, when a field’s serialized name differs from the source name you represent it like this:
# V1: Here's a field called "description". It pulls from "desc".
field :desc, name: :description
The LegacyOptions extension allows this to continue working.
V2 style
Since Blueprinter is a serializer, the DSL should describe the serialized result, not the source object. V2 reads much more naturally:
# V2: Here's a field called "description". It pulls from "desc".
field :description, source: :desc
Extractor option
V2 does not have a specific “extractor” concept, but the LegacyOptions extension allows your existing extractors to continue working.
field :foo, extractor: MyExtractor
V2 style
In V2 you can override Blueprinter’s field extraction using field blocks or the around_field_value, around_object_value, and around_collection_value extension hooks.
class MyExtractorExtension < Blueprinter::Extension
# @param ctx [Blueprinter::V2::Context::Field]
def extract(ctx)
# Instead of yielding to get the field value, extract and return it here
field_source = ctx.field.source
object = ctx.object
# ...
end
alias around_field_value extract
alias around_object_value extract
alias around_collection_value extract
end
Dynamic options
Associations in legacy/V1 provided an :options option on associations. It allowed you to define a Hash or Proc that would be merged into the render options.
It was provided as a work-around when legacy/V1 began freezing options, since options could no longer be used as an arbitrary “store” in field blocks, etc.
In V2 it’s enabled by the LegacyDynamicOptions extension. It’s recommended to only add this extension to specific blueprints that need it:
class WidgetBlueprint < ApplicationBlueprint
add Blueprinter::Extensions::LegacyDynamicOptions.new
association :category, CategoryBlueprint, options: ->(widget) {
# Will be merged into `ctx.options`
{ foo: widget.foo }
}
end
V2 style
V2 provides the ctx.store Hash for Blueprints and extensions to share information.
class WidgetBlueprint < ApplicationBlueprint
association :category, CategoryBlueprint do |widget, ctx|
cxt.store[:foo] = widget.foo
widget.category
end
end
New Features
V2 introduces several new concepts to Blueprinter. Some of these may have been mentioned in previous sections, but continue reading for a deep dive.
Formatters
Blueprinter V2 has a more flexible and ergonomic approach to formatting, allowing any class to define its own formatter in a single line:
class MyBlueprint < ApplicationBlueprint
format(Date) { |date| date.iso8601 }
format TrueClass, :boolean_str
format FalseClass, :boolean_str
def boolean_str(bool)
bool ? "Y" : "N"
end
end
If your formatter needs more information (field details, options passed to render, etc) use the around_field_value, around_object_value, and around_collection_value extension hooks. See Blueprinter::Extension for the full API, or get started with the Extension Guide.
Views are Blueprints
A view is an anonymous subclass of its Blueprint. There is little practical or technical distinction between “blueprints” and “views”.
This has two important consequences:
- The DSL is recursive: views can do anything Blueprints can do.
- Views are Ruby classes: views can do anything Ruby classes can do.
class MyBlueprint < ApplicationBlueprint
set :exclude_if_nil, true
add MyExtension.new
format Time, :iso8601
fields :name, :description
# This view is a subclass of MyBlueprint
view :my_view do
# Override inherited options
set :exclude_if_nil, false
# Add an extension
add OtherExtension.new
# Include a Ruby module. Only this view (and any child views) will have it.
include MyHelpers
# Override the formatter's method to ensure UTC time
def iso8601(t) = t.utc.iso8601
end
def iso8601(t) = t.iso8601
end
You can even subclass another Blueprint’s view!
class FooBlueprint < MyBlueprint[:my_view] do
# ...
end
The default view
The default view is simply an alias to the class:
MyBlueprint == MyBlueprint[:default]
=> true
And since views are their own Blueprints, each view has its own default view:
MyBlueprint[:my_view] == MyBlueprint[:my_view][:default]
=> true
Which brings us to the next topic: Nested Views.
Nested views
Because the V2 DSL is recursive, you can define views inside of views.
Each nested view inherits from its parent, which inherits from its parent, all the way up to ApplicationBlueprint and Blueprinter::V2::Base.
class MyBlueprint < ApplicationBlueprint
fields :name, :description
view :extended do
association :category, CategoryBlueprint
view :with_foo do
association :foo, FooBlueprint
end
view :with_bar do
association :bar, BarBlueprint
end
end
end
You can render nested views using dot syntax:
MyBlueprint["extended.with_foo"].render(widget).to_json
or nested Hash syntax:
MyBlueprint[:extended][:with_bar].render(widget).to_json
Partials
Partials allow you to compose your Blueprints and views from reusable pieces.
class MyBlueprint < ApplicationBlueprint
view :view_1 do
use :common_associations
# ...
end
view :view_2 do
use :common_associations
# ...
end
partial :common_associations do
association :foo, FooBlueprint
association :bar, [BarBlueprint]
end
end
You may only use partials defined in the current Blueprint/view (or a parent). To share code across completely separate Blueprints, use modules.
See the Blueprinter::V2::DSL docs for more info on partial and use.
More than just fields
Partials have access to the full DSL, so they can set options, add extensions, formatters, views, and even other partials.
class ApplicationBlueprint < Blueprinter::V2::Base
partial :exclude_nil_or_blank do
# Enable the built-in "skip if nil" option
set :exclude_if_nil, true
# Add an inline extension to skip blank fields
extension do
def around_field_value(ctx)
val = yield ctx
skip! if val.blank?
val
end
end
end
end
Blueprints or views that use the above partial will skip any nil or blank fields:
class MyBlueprint < ApplicationBlueprint
use :exclude_nil_or_blank
end
Modules
Any Ruby module can be extended with the Blueprinter DSL:
module MySharedBlueprintCode
extend Blueprinter::V2::DSL
set :exclude_if_nil, true
add MyExtension.new
format Time, :iso8601
format Date, :iso8601
field :foo
view :full do
association :bar, BarBlueprint
end
def iso8601(val) = val.iso8601
end
Then included into Blueprints:
class MyBlueprint < ApplicationBlueprint
include MySharedBlueprintCode
# ...
end
Or views:
class MyBlueprint < ApplicationBlueprint
# ...
view :my_view do
include MySharedBlueprintCode
# ...
end
end
Or partials:
class MyBlueprint < ApplicationBlueprint
# ...
partial :my_partial do
include MySharedBlueprintCode
# ...
end
end
Extensions
Extensions exist in legacy/V1, but they were added late and have limited functionality. Blueprinter V2 has been designed from the ground up with extensions in mind.
Writing extensions
See the Blueprinter::Extension docs for the full API, or read through the Extension Guide.
Using extensions
Extensions can be added to Blueprints, views, and partials. They’ll be inherited by child Blueprints or views.
# Add some extensions (append)
add MyExtension.new, MyOtherExtension.new
# Prepend an extension
add MyExtension.new, prepend: true
# Remove an extension by class or block
remove MyExtension
remove { |ext| ext.is_a? MyExtension }
# Prevent inheritance of extensions
exclude extensions: true
Bundled extensions
Blueprinter V2 comes bundled with the following extensions. See the Blueprinter::Extensions module for full documentation about each one.
MultiJson
Uses the multi_json gem to serialize JSON. See Blueprinter::Extensions::MultiJson.
OpenTelemetry
Instruments the serialization process so you can see which Blueprints or extensions are slowing things down. See Blueprinter::Extensions::OpenTelemetry.
FieldOrder
Customizes the field order in the serialized output. See Blueprinter::Extensions::FieldOrder.
V1 Compatibility Extensions
These extensions offer V1-compatibility for some options. They’re covered under the Compatible Changes section.
Blueprinter::Extensions::LegacyOptionsBlueprinter::Extensions::LegacyTransformer
Extension Guide
Is there some functionality you wish Blueprinter had? Building an extension might be the best way to add it!
This guide will walk you through building some sample extensions. While reading the docs for Blueprinter::Extension is the best way to learn
the entire API, this guide shows off some of the powerful things you can do.
If you need more inspiration, look over some real-world extensions like blueprinter-activerecord or Blueprinter’s bundled extensions (TODO link).
Basics
An extension will subclass Blueprinter::Extension and define one or more hook methods. (See Blueprinter::Extension for documentation about all hooks.)
class ExcludeIfBlankExtension < Blueprinter::Extension
def around_field_value(ctx)
# get the value by yielding to other extensions and Blueprinter's internals
val = yield ctx
# skip this field and halt further extensions if the value is blank
skip! if ctx.field.options[:exclude_if_blank] && val.blank?
# return the value for the next extension (or Blueprinter) to use
val
end
end
Then add the extension to a blueprint:
class MyBlueprint < Blueprinter::V2::Base
add ExcludeIfBlankExtension.new
field :name, exclude_if_blank: true
end
Context Objects
All extension hooks take a single argument: a context object. It contains the “context” of the current operation (Blueprint, options, field, the object, etc.) so you can react to it and, in some cases, alter it.
There are several different context object types. See the Blueprinter::V2::Context module for documentation on which hooks receive which types, and what can be done with them.
Exclude If Blank
Here’s a more fully formed version of the ExcludeIfBlank extension from the previous section.
It works just like the built-in exclude_if_nil option, but checks for blank? instead. It checks the Blueprint
options first, then allows fields to override it.
class ExcludeIfBlankExtension < Blueprinter::Extension
# @param ctx [Blueprinter::V2::Context::Field]
def around_field_value(ctx)
val = yield ctx
exclude = ctx.blueprint.options[:exclude_if_blank]
exclude = ctx.field.options[:exclude_if_blank] if ctx.field.options.key? :exclude_if_blank
skip! if exclude && val.blank?
val
end
end
Add it to your ApplicationBlueprint and use it in your blueprints.
class ApplicationBlueprint < Blueprinter::V2::Base
add ExcludeIfBlankExtension.new
end
class WidgetBlueprint < ApplicationBlueprint
field :name
field :description, exclude_if_blank: true
end
class CategoryBlueprint < ApplicationBlueprint
set :exclude_if_blank, true
field :name, exclude_if_blank: false
field :description
end
Custom Extractor
Blueprinter V2 automatically handles extraction from Hashes (symbol and string keys) and objects (public_send). If you have a more complex case, use the around_field_value, around_object_value, or around_collection_value extension hooks.
The following example extension allows you to define extraction behavior for a given class:
class CustomExtractor < Blueprinter::Extension
def initialize(klass, &extractor)
@klass = klass
@extractor = extractor
end
# @param ctx [Blueprinter::V2::Context::Field]
def around_field_value(ctx)
if ctx.object.is_a? @klass
# Use custom extraction
@extractor.call(ctx.field.source, ctx.object)
else
# Let Blueprinter (or another extension) handle extraction
yield ctx
end
end
# Same behavior for associations
alias around_object_value around_field_value
alias around_collection_value around_field_value
end
Then add it to your base Blueprint, or to a specific Blueprint:
class MyBlueprint < ApplicationBlueprint
add CustomExtractor.new(MyClass) { |attr, object|
# extract `attr` from `object` and return
}
end
Blueprint Decorator
This example adds metadata to the output of a Blueprint, similar to Legacy/V1’s transformer feature.
class DecoratorExtension < Blueprinter::Extension
def initialize(attr, &decorator)
@attr = attr
@decorator = decorator
end
# @param ctx [Blueprinter::V2::Context::Object]
def around_blueprint(ctx)
result = yield ctx
result[@attr] = @decorator.call(ctx.object)
result
end
end
Add the extension to whatever blueprints need metadata:
class MyBlueprint < ApplicationBlueprint
add DecoratorExtension.new(:metadata) { |object|
# extract and return metadata from the object
}
end
Telemetry
Blueprinter bundles the Blueprinter::Extensions::OpenTelemetry extension. But if that doesn’t work with your tooling, build your own!
class MyTelemetryExtension
def initialize(my_tel)
@my_tel = my_tel
end
# Create a span for object serialization
# @param ctx [Blueprinter::V2::Context::Object]
def around_serialize_object(ctx)
@my_tel.span("blueprint.object", blueprint: ctx.blueprint.to_s) do
yield ctx
end
end
# Create a span for collection serialization
# @param ctx [Blueprinter::V2::Context::Object]
def around_serialize_collection(ctx)
@my_tel.span("blueprint.collection", blueprint: ctx.blueprint.to_s) do
yield ctx
end
end
# Create a span for other extension hooks
# @param ctx [Blueprinter::V2::Context::Hook]
def around_hook(ctx)
@my_tel.span("blueprint.extension", extension: ctx.extension.class.name, hook: ctx.hook) do
yield
end
end
# Prevent `around_hook` from running around this extension's own hooks
def hidden? = true
end
Camelize Fields
This example alters the source of each field to use camel case, leaving the serialized name as-is.
class CamelizedSourceExtension < Blueprinter::Extension
def around_blueprint_init(ctx)
ctx.fields.each do |field|
field.source = field.source.to_s.camelize(:lower).to_sym
end
yield ctx
end
end
It’s the equivalent of doing this for every field:
field :foo_bar, source: :fooBar
YAML Serializer
This example adds YAML to Blueprinter. Why? Because we can.
class YamlSerializerExtension < Blueprinter::Extension
def around_result(ctx)
case ctx.format
when :yaml
# Get the Hash/Array result
result = yield ctx
# Convert it to YAML
yaml = YAML.dump result
# Return YAML, declaring it as already serialized
serialized yaml
else
# Let Blueprinter or another extension handle other formats
yield ctx
end
end
end
You can serialize to your custom format using the to method:
MyBlueprint.render(object).to(:yaml)
Extensions that serialize should be added FIRST
When you’re adding an extension that adds a serialization format, or replaces the built-it :json or :hash ones, be sure it’s listed first.
class ApplicationBlueprint < Blueprinter::V2::Base
add YamlSerializerExtension.new, OtherExtension.new
end
Performance
Any use of extensions has some performance implications, irrespective of what the extension code is doing. This can range anywhere from “essentially zero” to “noticible”. (See Blueprinter::Extension for documentation about which hooks are the most and least expensive.)
Given the number of extension hooks available, there are frequently multiple ways to implement a given behavior. Some of these will be more efficient than others.
Example: Camel case converter
Let’s say your source objects store fields in camelCase, but you want to serialize them using snake_case. You could define every field in your Blueprints like this:
field :some_field, source: :someField
But that’s tedious and error-prone. Let’s write an extension to do it for us.
Manually extract each field
The around_field_value hook and friends are the simplest way to implement it:
class CamelCaseSource < Blueprinter::Extension
# @param ctx [Blueprinter::V2::Context::Field]
def around_field_value(ctx)
attr = ctx.field.source_str.camelize
if ctx.object.is_a? Hash
ctx.object.key?(attr) ? ctx.object[attr] : ctx.object[attr.to_sym]
else
ctx.object.public_send(attr)
end
end
alias around_object_value around_field_value
alias around_collection_value around_field_value
end
But what about the performance? These hooks will run for every field on the Blueprint. If you’re adding this extension only to specific Blueprints that might be fine.
But what if someone adds it to ApplicationBlueprint? Then it will run for every field in every Blueprint in your application!
Transform each Blueprint’s result
The around_blueprint hook allows you to intercept and modify the serialized hash output of each Blueprint. If we define our Blueprints in camel case:
field :someField
Then we can convert the output to snake case:
class CamelCaseSource < Blueprinter::Extension
# @param ctx [Blueprinter::V2::Context::Object]
def around_blueprint(ctx)
# Get the serialized output from Blueprinter
result = yield ctx
# Convert it
snake_case result
result
end
def snake_case(hash)
hash.transform_keys! { |k| k.to_s.underscore }
hash.each_value { |v| snake_case v if v.is_a? Hash }
end
end
This runs only once per serialized object, so it’s a big improvement. But that could still be hundreds of times, or more, in a large result. Can we do better?
Change field config before render
What if we could alter the field definitions programatically, before anything is serialized? The around_blueprint_init hook runs only once per Blueprint during a render. It’s quite cheap, and it can alter field definitions.
class CamelCaseSource < Blueprinter::Extension
# @param ctx [Blueprinter::V2::Context::Init]
def around_blueprint_init(ctx)
ctx.fields.each do |field|
field.source = field.source_str.camelize.to_sym
end
yield ctx
end
This is effectively the same as setting source on each field in the Blueprint. There’s essentially zero runtime cost.
Legacy/V1 Docs
TODO copy from old README