2.0 Base Class Proposal
A proposed V2 base class.
Overview
- Combines concepts of "blueprint" and "view" (from #430).
- A view is just a subclass of the Blueprint.
- A nested view is a sublcass of its parent view.
- The
:defaultview is just an alias toself(i.e. the Blueprint class).
- Fields, partials, extensions, and options are inherited from the parent class/view, but can then be customized.
- Fields and associations are just structs. All official options are keyword args/struct members.
- Arbitrary options can still be passed (e.g. for use by extensions).
- "Partials" can be defined and included in views.
Notes
- There's no
MyBlueprint < Blueprinter[2.0]syntax in this PR, but we can add it later if we want. - More options for views and fields/associations will need to be added in later PRs.
- We'll have to rationalize which options become extension hooks and which don't in later PRs.
- Hoping @lessthanjacob's new rendering stuff can be used for both Legacy and V2 blueprints.
Defining
# "global" config goes here
class ApplicationBlueprint < Blueprinter::V2::Base
extensions << MyExtension.new
options.exclude_nil = true
field :id
end
class MyBlueprint < ApplicationBlueprint
field :name
association :category, CategoryBlueprint
# inherits from "default" view
view :extended do
exclude :name
association :widgets, WidgetBlueprint[:extended]
# inherits from "extended" view
view :plus do
# Uses a legacy blueprint
association :foo, LegacyFooBlueprint, view: :plus
end
end
end
# inherits from "MyBlueprint extended plus" view
class MyOtherBlueprint < MyBlueprint["extended.plus"]
...
end
# Much like in Rails, you can define partials for your views
class WidgetBlueprint < ApplicationBlueprint
field :name
partial :parts do
# Defining fields is the obvious use-case
association :parts, WidgetPartBlueprint
# But they can also use the full V2 API, including extensions, views and including other partials
extensions << MyExt.new
end
view :foo do
# Include the partial
use :parts
field :foo
end
view :bar do
# Include the partial
use :parts
field :bar
end
end
Rendering
# Render the default view
MyBlueprint.render(obj, opts)
MyBlueprint[:default].render(obj, opts)
# Render a named view
MyBlueprint[:extended].render(obj, opts)
MyBlueprint["extended"].render(obj, opts)
# Render a nested view
MyBlueprint["extended.plus"].render(obj, opts)
# which is syntactic sugar for:
MyBlueprint[:extended][:plus].render(obj, opts)
# Universal render call
view = "default" # or :foo, "foo.bar", :"foo.bar", etc
MyBlueprint[view].render(obj, opts)
# It COULD be made backwards-compatible, although I dislike the "Hash soup" approach.
# Maybe have this in 1.x to ease adoption then remove in 2.0?
MyBlueprint.render(obj, view: "extended.plus")
Reflection
# Here, default refers to MyBlueprint
MyBlueprint.reflections.keys
=> [:default, :extended, :"extended.plus"]
# If you reflect from a child view, the keys are relative
MyBlueprint[:extended].reflections.keys
=> [:default, :plus]
Alt view options:
WidgetBlueprint.render(obj, view: "extended.plus")
WidgetBlueprint["extended.plus"].render(obj)
What about extractor classes that could be provided to fields and associations? Is that something that is addressed in this PR? https://github.com/procore-oss/blueprinter/blob/b90d251d7098249b6760493a4c414db4ba920c43/lib/blueprinter/field.rb#L11
We are currently using custom extractors to cache the values of extracted fields, and to detect infinite recursion while extracting a field (field references itself). See the related issue about extractor configurability. https://github.com/procore-oss/blueprinter/issues/404
What about extractor classes that could be provided to fields and associations? Is that something that is addressed in this PR?
We're still looking at how we want to implement extractors, and a few other things, in V2. Those will be follow-up PRs.
For posterity (as discussed in the meeting): In my mind, the important part of this PR is the view/class inheritance structure and associated DSL. That is: how fields, associations, partials, options, and extensions are inherited from a parent class/view into child classes/views in a consistent, predictable manner, obviating the need for global config and unifying the "blueprint" and "view" concepts.
The included implementations of field, V2::Field, V2::Options etc are meant as starting points for follow up discussions and PRs. (Not to mention extension hooks, extractors, etc.) My suspicion is that we'll want to design the renderer/serializer sooner rather than later, and that will tell us a lot about how we'll want to handle those details.