blueprinter icon indicating copy to clipboard operation
blueprinter copied to clipboard

2.0 Base Class Proposal

Open jhollinger opened this issue 1 year ago • 4 comments

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 :default view is just an alias to self (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]

jhollinger avatar Jul 09 '24 17:07 jhollinger

Alt view options:

WidgetBlueprint.render(obj, view: "extended.plus")
WidgetBlueprint["extended.plus"].render(obj)

jhollinger avatar Jul 09 '24 20:07 jhollinger

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

william-stacken avatar Sep 26 '24 09:09 william-stacken

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.

jhollinger avatar Oct 01 '24 15:10 jhollinger

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.

jhollinger avatar Oct 15 '24 21:10 jhollinger