procore-oss / blueprinter

Simple, Fast, and Declarative Serialization Library for Ruby
MIT License
1.14k stars 109 forks source link

Design New Extension Hooks #441

Open jhollinger opened 4 months ago

jhollinger commented 4 months ago

As pointed out in #417 and elsewhere, we have many configuration options restricted to certain API levels (global config, Blueprint, view, and field). A frequent request is "make config X available at API level Y". Each request starts familiar discussions about overrides and inheritance, and there are often several reasonable but mutually-exclusive paths forward.

While it may not be appropriate for every option to migrate to an extension hook, it certainly makes sense for extractors, transformers, and [date] formatters in 2.0. The goal of this issue to is to design these config-as-extension-hooks, as well as call out config options that might not make sense as hooks (e.g. maybe boolean options?).

NOTE: This issue is not to implement every hook or option, but to document what new hooks we need (and potentially their call signatures).

In 2.0, whether hooks or options, they'll all follow the same inheritance and override rules as fields/associations: Class/view > class/view > class/view, etc. fields/associations would have the final override (when applicable).

github-actions[bot] commented 2 months ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

jhollinger commented 3 weeks ago

Extracted from #479:

Formatters

Adding a formatter should be easy. Forcing people to learn "extensions" seems a little heavy. What if formatters were part of the Blueprint DSL?

class MyBlueprint < ApplicationBlueprint
  format(Date) { |date| date.iso8601 }
  format Time, :time_fmt

  def time_fmt(t)
    t.iso8601
  end
end

Hooks

Extractors

While we could make it work, I don't think extractors are a great case for extension hooks. You can have N extensions, but we only ever need one extractor for a given field.

The idea is provide a base extractor that behaves like our current collection of them. People can inherit from it and call super as needed.

class MyExtractor < Blueprinter::V2::Extractor
  def field(blueprint, field, object, options)
    if object.is_a? Something
      # special behavior
    else
      super
    end
  end

  def object(blueprint, field, object, options)
    # ...
  end

  def collection(blueprint, field, object, options)
    # ...
  end
end

class MyBlueprint < ApplicationBlueprint
  options[:extractor] = MyExtractor

  field :name
  field :description, extractor: OtherExtractor
end