Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
e127ad4
V2 serializer refactor for huge perf improvement
jhollinger Apr 15, 2026
931b571
Have dedicated structs for field reflection so people don't depend on…
jhollinger Apr 22, 2026
59a8767
Add options to reflections
jhollinger Apr 22, 2026
716f439
Cleanup
jhollinger Apr 24, 2026
3ce210a
Reduce V2 serializer allocations by ~23% via fast path
scottmyron Apr 23, 2026
41ed937
Get back to a single extract & serialization path. Sacrifices a *litt…
jhollinger Apr 28, 2026
3b8bb92
Ensure field conditional & default status is checked after copying do…
jhollinger Apr 28, 2026
923cf2c
Too difficult to use separate internal/reflected field defs. Use inte…
jhollinger Apr 28, 2026
d395ee3
Allow options to be passed when multiple fields are defined with `fie…
jhollinger Apr 28, 2026
2d76196
Centralize incrementing serialization depth
jhollinger Apr 29, 2026
595a6ac
Test to prove there's a way to share partials across classes
jhollinger Apr 29, 2026
bf69ed6
Replace instance_exec with simple calls in most cases.
jhollinger Apr 30, 2026
d9ff42e
Simplify extraction code
jhollinger Apr 30, 2026
f5131f6
Rename Context::Render to Context::Init
jhollinger Apr 30, 2026
1efcb67
Only allow around_result to change options. And don't freeze them unt…
jhollinger May 1, 2026
c8c047c
Since V2 doesn't currently support init-per-render extensions, it doe…
jhollinger May 1, 2026
f1d3a97
Move ExtensionHelpers back into Extension since it's only used in one…
jhollinger May 1, 2026
f1d3de5
More granular decisions on allocating field context objects (will sav…
jhollinger May 1, 2026
25e7ec7
Rename the 'from' option to 'source'. Reads much better in the docs
jhollinger May 1, 2026
d3ca0b8
Allow blueprint options and most field attributes to be modified by a…
jhollinger May 4, 2026
a3e3f85
Add back the around_blueprint hook (with optimized context obj alloca…
jhollinger May 5, 2026
d587705
Optimization for around_blueprint_init when nothing but field order o…
jhollinger May 5, 2026
23db99f
Put all the field classes under a module
jhollinger May 5, 2026
776f348
'default' Procs/methods don't need the value. For 'default_if', pass …
jhollinger May 8, 2026
e62dfac
Rename the core Wrapper extension to Root.
jhollinger May 8, 2026
69b8ee3
Extensions to make if, unless, and default_if V1-compatible
jhollinger May 8, 2026
6cbbe6d
Extension to make V1's 'name' option work
jhollinger May 10, 2026
f0d78a5
Extension to make V1's 'extractor' option work
jhollinger May 11, 2026
4702d88
Don't create field contexts if the block doesn't use them
jhollinger May 11, 2026
9d6cbbc
Rubocop and cleanup
jhollinger May 12, 2026
ddfa30f
Alias 'excludes' to 'exclude' for backwards compatibility
jhollinger May 12, 2026
ca3f43f
Rename Core::Json to Core::Format. Ensure it's the first/last around_…
jhollinger May 12, 2026
5dd2716
An extension to run legacy transformers
jhollinger May 13, 2026
3a8740d
Accept Proc blueprints in V2. Also, a better impl for accesing V1 blu…
jhollinger May 14, 2026
c065be7
Extension (and changes) to support legacy 'dynamic options' Hashes an…
jhollinger May 16, 2026
5c3ff38
Support legacy render_as_hash/render_as_json
jhollinger May 27, 2026
c15f238
Use the eigenclass for V2::Base class methods
jhollinger May 27, 2026
0943ccb
Make `use` into a propper macro, expanding it in place.
jhollinger May 28, 2026
48ce991
Ditch 'options' and 'extensions' blocks for set, add, and associated …
jhollinger May 31, 2026
6e5c009
Views must be node-driven (like everything else) so that views from p…
jhollinger Jun 2, 2026
39ecaf5
Differentiate between view_path (foo.bar) and view_name (bar)
jhollinger Jun 3, 2026
222ba6d
Move node evaluation to its own class, leaving room for rendering met…
jhollinger Jun 3, 2026
395ed87
Only eval on render or reflection. Allows Blueprints to reference the…
jhollinger Jun 3, 2026
4212fdd
A unified and consistent approach to excluding fields, options, and e…
jhollinger Jun 5, 2026
66ec478
Allow 'remove' to accept a block to dynamically remove extensions
jhollinger Jun 5, 2026
986f7b4
Allow excluding formatters. Use 'exclude' with options instead of ded…
jhollinger Jun 8, 2026
c8dee46
Clean up Evaluator
jhollinger Jun 9, 2026
7466022
Keep track of `use` callsites and include them in errors about invali…
jhollinger Jun 9, 2026
bdd4792
In the DSL, separate excluding inherited elements from excluding elem…
jhollinger Jun 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions lib/blueprinter/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
require_relative 'reflection'
require_relative 'rendering'
require_relative 'view_collection'
require_relative 'view_wrapper'

module Blueprinter
class Base
Expand Down Expand Up @@ -372,6 +371,17 @@ def view_collection
@_view_collection ||= ViewCollection.new
end

# For compatibility with V2's DSL
#
# @return [Blueprinter::Base] An anonymous subclass that renders a specific view
def [](view_name)
raise Errors::UnknownView, "View '#{view_name}' could not be found in Blueprint '#{name}'" unless view? view_name

Class.new(self) do
@forced_view_name = view_name
end
end

private

attr_accessor :view_scope
Expand All @@ -389,14 +399,5 @@ def association_extractor
@_association_extractor ||= AssociationExtractor.new
end
end

# For compatibility with V2
#
# @return [Blueprinter::ViewWrapper]
def self.[](view_name)
raise Errors::UnknownView, "View '#{view_name}' could not be found in Blueprint '#{name}'" unless view? view_name

ViewWrapper.new(self, view_name)
end
end
end
1 change: 1 addition & 0 deletions lib/blueprinter/empty_types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module Blueprinter

module EmptyTypes
include TypeHelpers
extend self

private

Expand Down
52 changes: 5 additions & 47 deletions lib/blueprinter/extension.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ module Blueprinter
# - pre_render
#
class Extension
include V2::Helpers

HOOKS = %i[
around_hook
around_result
Expand All @@ -41,53 +39,13 @@ def self.hooks
# If this returns true, around_hook will not be called when this extension's hooks are run. Used by core extensions.
def hidden? = false

# around_result TODO
# @param context [Blueprinter::V2::Context::Result]

# around_serialize_object: Runs around serialization of a Blueprint object.
# @param context [Blueprinter::V2::Context::Object]

# around_serialize_collection: Runs around serialization of a Blueprint collection.
# @param context [Blueprinter::V2::Context::Object]

# around_blueprint: Runs around serialization of every Blueprint.
# @param context [Blueprinter::V2::Context::Object]

# around_field_value TODO

# around_object_value TODO

# around_collection_value TODO

# blueprint_fields: Returns the fields that should be included in the correct order. Default is all fields in the order
# in which they were defined.
# NOTE If there are multiple blueprint_fields hooks, only the last one is called.
# NOTE Only runs once per Blueprint per render.
# @param context [Blueprinter::V2::Context::Render]
# @return [Array<Blueprinter::V2::Fields::Field|Blueprinter::V2::Fields::Object|Blueprinter::V2::Fields::Collection>]

# blueprint_setup: Called once per blueprint per render. A common use is to pre-calculate certain options
# and cache them in context.data, so we don't have to recalculate them for every field.
# @param context [Blueprinter::V2::Context::Render]

# around_hook: Instrument extension hook calls. MUST yield!
# @param extension [Blueprinter::Extension] Instance of the extension
# @param hook [Symbol] Name of hook being called

# pre_render: Called eary during "render" in V1, this method receives the object to be rendered and
# may return a modified (or new) object to be rendered.
# @param object [Object] The object to be rendered
# @param blueprint [Class] The Blueprinter class
# @param view [Symbol] The blueprint view
# @param options [Hash] Options passed to "render"
# @return [Object] The object to continue rendering

private
# Skip the current field and halt further field hooks
def skip! = throw V2::Serializer::SIGNAL, V2::Serializer::SIG_SKIP

# Helper for around_result hooks to declare that a result is "final"
def final(val) = V2::Context::Final.new(val)
def serialized(val) = V2::Context::Serialized.new(val)

# Helper for around_result hooks to check if a previous hook has declared a result "final"
def final?(val) = val.is_a? V2::Context::Final
# Helper for around_result hooks to check if a previous hook has declared a result "serialized"
def serialized?(val) = val.is_a? V2::Context::Serialized
end
end
6 changes: 6 additions & 0 deletions lib/blueprinter/extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ module Blueprinter
# Optional extensions for applications to pull in
module Extensions
autoload :FieldOrder, 'blueprinter/extensions/field_order'
autoload :LegacyConditionals, 'blueprinter/extensions/legacy_conditionals'
autoload :LegacyDefaultIf, 'blueprinter/extensions/legacy_default_if'
autoload :LegacyDynamicOptions, 'blueprinter/extensions/legacy_dynamic_options'
autoload :LegacyExtractorOption, 'blueprinter/extensions/legacy_extractor_option'
autoload :LegacyRenameField, 'blueprinter/extensions/legacy_rename_field'
autoload :LegacyTransformer, 'blueprinter/extensions/legacy_transformer'
autoload :MultiJson, 'blueprinter/extensions/multi_json'
autoload :OpenTelemetry, 'blueprinter/extensions/open_telemetry'
autoload :ViewOption, 'blueprinter/extensions/view_option'
Expand Down
2 changes: 1 addition & 1 deletion lib/blueprinter/extensions/field_order.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def initialize(&sorter)
@sorter = sorter
end

# @param ctx [Blueprinter::V2::Context::Render]
# @param ctx [Blueprinter::V2::Context::Init]
def around_blueprint_init(ctx)
ctx.fields = ctx.fields.sort(&@sorter)
yield ctx
Expand Down
39 changes: 39 additions & 0 deletions lib/blueprinter/extensions/legacy_conditionals.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

module Blueprinter
module Extensions
#
# Support for Legacy/V1's legacy conditionals. V2 continue to work.
#
class LegacyConditionals < Extension
# @!visibility private
V1_ARITY = 3

# @param ctx [Blueprinter::V2::Context::Init]
# @!visibility private
def around_blueprint_init(ctx)
# Convert blueprint if/unless options
ctx.blueprint.options[:if] = convert_v1(ctx.blueprint.options[:if]) if ctx.blueprint.options[:if]
ctx.blueprint.options[:unless] = convert_v1(ctx.blueprint.options[:unless]) if ctx.blueprint.options[:unless]

# Convert field if/unless options
ctx.fields.each do |field|
field.options[:if] = convert_v1(field.options[:if]) if field.options[:if]
field.options[:unless] = convert_v1(field.options[:unless]) if field.options[:unless]
end

yield ctx
end

private

def convert_v1(cond)
if cond.arity == V1_ARITY
->(ctx) { cond.call(ctx.field.source, ctx.object, ctx.options) }
else
cond
end
end
end
end
end
37 changes: 37 additions & 0 deletions lib/blueprinter/extensions/legacy_default_if.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

module Blueprinter
module Extensions
#
# Support for Legacy/V1's `default_if` options.
#
class LegacyDefaultIf < Extension
# @param ctx [Blueprinter::V2::Context::Init]
# @!visibility private
def around_blueprint_init(ctx)
if (default_if = ctx.blueprint.options[:default_if])
ctx.blueprint.options[:default_if] = convert_v1(default_if)
end

ctx.fields.each do |field|
if (default_if = field.options[:default_if])
field.options[:default_if] = convert_v1(default_if)
end
end

yield ctx
end

private

def convert_v1(cond)
case cond
when ::Blueprinter::EMPTY_COLLECTION, ::Blueprinter::EMPTY_HASH, ::Blueprinter::EMPTY_STRING
->(_ctx, value) { EmptyTypes.send(:use_default_value?, value, cond) }
else
cond
end
end
end
end
end
29 changes: 29 additions & 0 deletions lib/blueprinter/extensions/legacy_dynamic_options.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

module Blueprinter
module Extensions
#
# Support for Legacy/V1's `:options` option on associations.
#
class LegacyDynamicOptions < Extension
# @!visibility private
def apply(ctx)
ctx.fields.each do |field|
additional_opts =
case (opts = field.options[:options])
when Hash then opts
when Proc then opts.call(ctx.object)
end
ctx.options = ctx.options.merge(additional_opts).freeze if additional_opts
end
yield ctx
end

# @!visibility private
alias around_serialize_object apply

# @!visibility private
alias around_serialize_collection apply
end
end
end
28 changes: 28 additions & 0 deletions lib/blueprinter/extensions/legacy_extractor_option.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module Blueprinter
module Extensions
#
# Support for Legacy/V1's `extractor` option.
#
class LegacyExtractorOption < Extension
# @!visibility private
def extract(ctx)
extractor_class = ctx.field.options[:extractor] || ctx.blueprint.options[:extractor]
return yield ctx if extractor_class.nil?

extractor = ctx.store[extractor_class.object_id] ||= extractor_class.new
extractor.extract(ctx.field.source, ctx.object, ctx.options, ctx.field.options)
end

# @!visibility private
alias around_field_value extract

# @!visibility private
alias around_object_value extract

# @!visibility private
alias around_collection_value extract
end
end
end
22 changes: 22 additions & 0 deletions lib/blueprinter/extensions/legacy_rename_field.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Blueprinter
module Extensions
#
# Support for Legacy/V1's `name` option.
#
class LegacyRenameField < Extension
# @param ctx [Blueprinter::V2::Context::Init]
# @!visibility private
def around_blueprint_init(ctx)
ctx.fields.each do |field|
if (name = field.options[:name])
field.source = field.name
field.name = name
end
end
yield ctx
end
end
end
end
26 changes: 26 additions & 0 deletions lib/blueprinter/extensions/legacy_transformer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module Blueprinter
module Extensions
#
# Support for Legacy/V1's transformers.
#
class LegacyTransformer < Extension
# @param *transformers [Class] One or more transformers (Blueprinter::Transformer)
def initialize(*transformers)
@transformers = transformers
end

# @param ctx [Blueprinter::V2::Context::Object]
# @!visibility private
def around_blueprint(ctx)
hash = yield ctx
@transformers.each do |klass|
transformer = ctx.store[klass.object_id] ||= klass.new
transformer.transform(hash, ctx.object, ctx.options)
end
hash
end
end
end
end
14 changes: 5 additions & 9 deletions lib/blueprinter/extensions/multi_json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,11 @@ def initialize(options = {})

# @param ctx [Blueprinter::V2::Context::Result]
def around_result(ctx)
case ctx.format
when :json
ctx.format = :hash
result = yield ctx
opts = ctx.options[:multi_json] ? @options.merge(ctx.options[:multi_json]) : @options
final ::MultiJson.dump(result, opts)
else
yield ctx
end
result = yield ctx
return result unless ctx.format == :json

opts = ctx.options[:multi_json] ? @options.merge(ctx.options[:multi_json]) : @options
serialized ::MultiJson.dump(result, opts)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/blueprinter/extensions/view_option.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module Blueprinter
module Extensions
#
# An optional, built-in extension for a ":view" option on render.
# Support for Legacy/V1's `:view` option on `render`.
#
class ViewOption < Extension
# @param ctx [Blueprinter::V2::Context::Result]
Expand Down
5 changes: 2 additions & 3 deletions lib/blueprinter/extractors/association_extractor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,10 @@ def extract_v2(value, blueprint, local_options, options)
store = local_options[:v2_store] || {}
depth = local_options[:v2_depth] || 1
instances = local_options[:v2_instances] || V2::InstanceCache.new
serializer = instances.serializer(blueprint[view], local_options.except(:v2_instances), store, depth + 1)
if value.is_a?(Enumerable) && !value.is_a?(Hash)
serializer.collection(value, depth: depth + 1)
blueprint[view].serializer.collection(value, local_options, instances:, store:, depth: depth + 1)
else
serializer.object(value, depth: depth + 1)
blueprint[view].serializer.object(value, local_options, instances:, store:, depth: depth + 1)
end
end

Expand Down
Loading