From d9a44385ed02f6fd08f0d852ca22d904dd5225b8 Mon Sep 17 00:00:00 2001 From: Jordan Hollinger Date: Tue, 9 Jul 2024 12:30:32 -0400 Subject: [PATCH] V2 base class Signed-off-by: Jordan Hollinger --- lib/blueprinter.rb | 1 + lib/blueprinter/errors.rb | 1 + lib/blueprinter/errors/unknown_view.rb | 7 ++ lib/blueprinter/v2.rb | 74 +++++++++++++++++++++ spec/v2/name_spec.rb | 91 ++++++++++++++++++++++++++ 5 files changed, 174 insertions(+) create mode 100644 lib/blueprinter/errors/unknown_view.rb create mode 100644 lib/blueprinter/v2.rb create mode 100644 spec/v2/name_spec.rb diff --git a/lib/blueprinter.rb b/lib/blueprinter.rb index 32ae3346..8d305383 100644 --- a/lib/blueprinter.rb +++ b/lib/blueprinter.rb @@ -5,4 +5,5 @@ module Blueprinter autoload :BlueprinterError, 'blueprinter/blueprinter_error' autoload :Errors, 'blueprinter/errors' autoload :Extension, 'blueprinter/extension' + autoload :V2, 'blueprinter/v2' end diff --git a/lib/blueprinter/errors.rb b/lib/blueprinter/errors.rb index a3adb656..2d4d98bb 100644 --- a/lib/blueprinter/errors.rb +++ b/lib/blueprinter/errors.rb @@ -3,5 +3,6 @@ module Blueprinter module Errors autoload :InvalidBlueprint, 'blueprinter/errors/invalid_blueprint' + autoload :UnknownView, 'blueprinter/errors/unknown_view' end end diff --git a/lib/blueprinter/errors/unknown_view.rb b/lib/blueprinter/errors/unknown_view.rb new file mode 100644 index 00000000..eb2ab87d --- /dev/null +++ b/lib/blueprinter/errors/unknown_view.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Blueprinter + module Errors + class UnknownView < Blueprinter::BlueprinterError; end + end +end diff --git a/lib/blueprinter/v2.rb b/lib/blueprinter/v2.rb new file mode 100644 index 00000000..075ae422 --- /dev/null +++ b/lib/blueprinter/v2.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module Blueprinter + class V2 + class << self + attr_accessor :views, :fields, :extensions, :blueprint_name + end + + self.views = {} + self.fields = {} + self.extensions = [] + self.blueprint_name = [] + + # Initialize subclass + def self.inherited(subclass) + subclass.views = {} + subclass.fields = fields.dup + subclass.extensions = extensions.dup + subclass.blueprint_name = subclass.name ? [subclass.name] : blueprint_name.dup + end + + # A descriptive name for the Blueprint view, e.g. "WidgetBlueprint:extended" + def self.inspect + to_s + end + + # A descriptive name for the Blueprint view, e.g. "WidgetBlueprint:extended" + def self.to_s + blueprint_name.join ':' + end + + # Access a child view + def self.[](view) + views.fetch(view) + rescue KeyError + raise Blueprinter::Errors::UnknownView, "View '#{view}' could not be found in Blueprint '#{self}'" + end + + # Define a new child view, which is a subclass of self + def self.view(name, &definition) + views[name] = Class.new(self) + views[name].blueprint_name << name + views[name].class_eval(&definition) if definition + views[name] + end + + # Define a field + # rubocop:todo Lint/UnusedMethodArgument + def self.field(name, options = {}) + fields[name] = 'TODO' + end + + # Define an association + def self.association(name, blueprint, options = {}) + fields[name] = 'TODO' + end + + def self.render(obj, options = {}) + new.render(obj, options) + end + + def render(obj, options = {}) + # TODO: call an external Render module/class, passing in self, obj, and options. + # + # I propose this new renderer (possibly shared with 1.x) would have an "outer" and + # "inner" API. The "inner" API would be used when rendering nested Blueprints. The + # "outer" API would only be called here. + # + # This design would allow for some render hooks to only be called ONCE per render (baring + # a field/association block calling "render" again), and others to be called on every + # nested Blueprint. This would fix some persistent issues with blueprinter-activerecord. + end + end +end diff --git a/spec/v2/name_spec.rb b/spec/v2/name_spec.rb new file mode 100644 index 00000000..e4220170 --- /dev/null +++ b/spec/v2/name_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +describe "Blueprinter::V2 Names" do + context 'const named Blueprints' do + class NamedBlueprint < Blueprinter::V2 + view :extended + end + + it 'should have a base name' do + expect(NamedBlueprint.to_s).to eq "NamedBlueprint" + expect(NamedBlueprint.inspect).to eq "NamedBlueprint" + end + + it 'should find a view by name' do + expect(NamedBlueprint[:extended].to_s).to eq "NamedBlueprint:extended" + expect(NamedBlueprint[:extended].inspect).to eq "NamedBlueprint:extended" + end + + it 'should raise for an invalid view name' do + expect { NamedBlueprint[:wrong_name] }.to raise_error( + Blueprinter::Errors::UnknownView, + "View 'wrong_name' could not be found in Blueprint 'NamedBlueprint'" + ) + end + end + + context 'manually named Blueprints' do + let(:blueprint) do + Class.new(Blueprinter::V2) do + blueprint_name << "MyBlueprint" + view :extended + end + end + + it 'should have no base name' do + expect(blueprint.to_s).to eq "MyBlueprint" + expect(blueprint.inspect).to eq "MyBlueprint" + end + + it 'should find a view by name' do + expect(blueprint[:extended].to_s).to eq "MyBlueprint:extended" + expect(blueprint[:extended].inspect).to eq "MyBlueprint:extended" + end + end + + context 'anonymous Blueprints' do + let(:blueprint) do + Class.new(Blueprinter::V2) do + view :extended + end + end + + it 'should have no base name' do + expect(blueprint.to_s).to eq "" + expect(blueprint.inspect).to eq "" + end + + it 'should find a view by name' do + expect(blueprint[:extended].to_s).to eq "extended" + expect(blueprint[:extended].inspect).to eq "extended" + end + end + + context 'deeply nested Blueprints' do + let(:blueprint) do + Class.new(Blueprinter::V2) do + blueprint_name << "MyBlueprint" + + view :foo do + view :bar do + view :zorp + end + end + end + end + + it 'should find deeply nested names' do + expect(blueprint.to_s).to eq "MyBlueprint" + expect(blueprint.inspect).to eq "MyBlueprint" + + expect(blueprint[:foo].to_s).to eq "MyBlueprint:foo" + expect(blueprint[:foo].inspect).to eq "MyBlueprint:foo" + + expect(blueprint[:foo][:bar].to_s).to eq "MyBlueprint:foo:bar" + expect(blueprint[:foo][:bar].inspect).to eq "MyBlueprint:foo:bar" + + expect(blueprint[:foo][:bar][:zorp].to_s).to eq "MyBlueprint:foo:bar:zorp" + expect(blueprint[:foo][:bar][:zorp].inspect).to eq "MyBlueprint:foo:bar:zorp" + end + end +end