diff --git a/lib/blueprinter/v2.rb b/lib/blueprinter/v2.rb index 6ee4f654..08e13484 100644 --- a/lib/blueprinter/v2.rb +++ b/lib/blueprinter/v2.rb @@ -1,116 +1,3 @@ # frozen_string_literal: true -require 'blueprinter/v2/dsl' -require 'blueprinter/v2/options' -require 'blueprinter/v2/reflection' -require 'blueprinter/v2/view_builder' - -module Blueprinter - # Base class for V2 Blueprints - class V2 - extend DSL - extend Reflection - - class << self - # Options set on this Blueprint - attr_accessor :options - # Extensions set on this Blueprint - attr_accessor :extensions - # @api private The fully-qualified name, e.g. "MyBlueprint", or "MyBlueprint.foo.bar" - attr_accessor :blueprint_name - # @api private - attr_accessor :views, :fields, :excludes, :partials, :used_partials, :eval_mutex - end - - self.views = ViewBuilder.new(self) - self.fields = {} - self.excludes = [] - self.partials = {} - self.used_partials = [] - self.extensions = [] - self.options = Options.new(DEFAULT_OPTIONS) - self.blueprint_name = name - self.eval_mutex = Mutex.new - - # Initialize subclass - def self.inherited(subclass) - subclass.views = ViewBuilder.new(subclass) - subclass.fields = fields.transform_values(&:dup) - subclass.excludes = [] - subclass.partials = partials.dup - subclass.used_partials = [] - subclass.extensions = extensions.dup - subclass.options = options.dup - subclass.blueprint_name = subclass.name || blueprint_name - subclass.eval_mutex = Mutex.new - end - - # A descriptive name for the Blueprint view, e.g. "WidgetBlueprint.extended" - def self.inspect - blueprint_name - end - - # A descriptive name for the Blueprint view, e.g. "WidgetBlueprint.extended" - def self.to_s - blueprint_name - end - - # Append the sub-view name to blueprint_name - # @api private - def self.append_name(name) - self.blueprint_name = "#{blueprint_name}.#{name}" - end - - # - # Access a child view. - # - # MyBlueprint[:extended] - # MyBlueprint["extended.plus"] or MyBlueprint[:extended][:plus] - # - # @param view [Symbol|String] Name of the view, e.g. :extended, "extended.plus" - # @return [Class] A descendent of Blueprinter::V2 - # - def self.[](view) - eval! unless @evaled - view.to_s.split('.').reduce(self) do |blueprint, child| - blueprint.views[child.to_sym] || - raise(Errors::UnknownView, "View '#{child}' could not be found in Blueprint '#{blueprint}'") - end - end - - # Apply partials and field exclusions - # @api private - def self.eval!(lock = true) - return if @evaled - - if lock - eval_mutex.synchronize { run_eval! unless @evaled } - else - run_eval! - end - end - - # @api private - def self.run_eval! - used_partials.each do |name| - if !(p = partials[name]) - raise Errors::UnknownPartial, "Partial '#{name}' could not be found in Blueprint '#{self}'" - end - class_eval(&p) - end - - excludes.each { |f| fields.delete f } - @evaled = true - end - - # Render the object - def self.render(obj, options = {}) - new.render(obj, options) - end - - # Render the object - def render(obj, options = {}) - # TODO: call an external Render module/class, passing in self, obj, and options - end - end -end +require 'blueprinter/v2/base' diff --git a/lib/blueprinter/v2/association.rb b/lib/blueprinter/v2/association.rb index fb872503..c091d0ea 100644 --- a/lib/blueprinter/v2/association.rb +++ b/lib/blueprinter/v2/association.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Blueprinter - class V2 + module V2 Association = Struct.new( :name, :blueprint, diff --git a/lib/blueprinter/v2/base.rb b/lib/blueprinter/v2/base.rb new file mode 100644 index 00000000..560c4b4a --- /dev/null +++ b/lib/blueprinter/v2/base.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require 'blueprinter/v2/dsl' +require 'blueprinter/v2/options' +require 'blueprinter/v2/reflection' +require 'blueprinter/v2/view_builder' + +module Blueprinter + # Base class for V2 Blueprints + module V2 + class Base + extend DSL + extend Reflection + + class << self + # Options set on this Blueprint + attr_accessor :options + # Extensions set on this Blueprint + attr_accessor :extensions + # @api private The fully-qualified name, e.g. "MyBlueprint", or "MyBlueprint.foo.bar" + attr_accessor :blueprint_name + # @api private + attr_accessor :views, :fields, :excludes, :partials, :used_partials, :eval_mutex + end + + self.views = ViewBuilder.new(self) + self.fields = {} + self.excludes = [] + self.partials = {} + self.used_partials = [] + self.extensions = [] + self.options = Options.new(DEFAULT_OPTIONS) + self.blueprint_name = name + self.eval_mutex = Mutex.new + + # Initialize subclass + def self.inherited(subclass) + subclass.views = ViewBuilder.new(subclass) + subclass.fields = fields.transform_values(&:dup) + subclass.excludes = [] + subclass.partials = partials.dup + subclass.used_partials = [] + subclass.extensions = extensions.dup + subclass.options = options.dup + subclass.blueprint_name = subclass.name || blueprint_name + subclass.eval_mutex = Mutex.new + end + + # A descriptive name for the Blueprint view, e.g. "WidgetBlueprint.extended" + def self.inspect + blueprint_name + end + + # A descriptive name for the Blueprint view, e.g. "WidgetBlueprint.extended" + def self.to_s + blueprint_name + end + + # Append the sub-view name to blueprint_name + # @api private + def self.append_name(name) + self.blueprint_name = "#{blueprint_name}.#{name}" + end + + # + # Access a child view. + # + # MyBlueprint[:extended] + # MyBlueprint["extended.plus"] or MyBlueprint[:extended][:plus] + # + # @param view [Symbol|String] Name of the view, e.g. :extended, "extended.plus" + # @return [Class] A descendent of Blueprinter::V2::Base + # + def self.[](view) + eval! unless @evaled + view.to_s.split('.').reduce(self) do |blueprint, child| + blueprint.views[child.to_sym] || + raise(Errors::UnknownView, "View '#{child}' could not be found in Blueprint '#{blueprint}'") + end + end + + # Apply partials and field exclusions + # @api private + def self.eval!(lock = true) + return if @evaled + + if lock + eval_mutex.synchronize { run_eval! unless @evaled } + else + run_eval! + end + end + + # @api private + def self.run_eval! + used_partials.each do |name| + if !(p = partials[name]) + raise Errors::UnknownPartial, "Partial '#{name}' could not be found in Blueprint '#{self}'" + end + class_eval(&p) + end + + excludes.each { |f| fields.delete f } + @evaled = true + end + + # Render the object + def self.render(obj, options = {}) + new.render(obj, options) + end + + # Render the object + def render(obj, options = {}) + # TODO: call an external Render module/class, passing in self, obj, and options + end + end + end +end diff --git a/lib/blueprinter/v2/dsl.rb b/lib/blueprinter/v2/dsl.rb index 817b81b1..b759d1e4 100644 --- a/lib/blueprinter/v2/dsl.rb +++ b/lib/blueprinter/v2/dsl.rb @@ -4,7 +4,7 @@ require 'blueprinter/v2/field' module Blueprinter - class V2 + module V2 # Methods for defining Blueprint fields and views module DSL # diff --git a/lib/blueprinter/v2/field.rb b/lib/blueprinter/v2/field.rb index f093daca..f9cc419a 100644 --- a/lib/blueprinter/v2/field.rb +++ b/lib/blueprinter/v2/field.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Blueprinter - class V2 + module V2 Field = Struct.new( :name, :from, diff --git a/lib/blueprinter/v2/options.rb b/lib/blueprinter/v2/options.rb index 19094715..edc24523 100644 --- a/lib/blueprinter/v2/options.rb +++ b/lib/blueprinter/v2/options.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Blueprinter - class V2 + module V2 Options = Struct.new( :exclude_nil, keyword_init: true diff --git a/lib/blueprinter/v2/reflection.rb b/lib/blueprinter/v2/reflection.rb index e2f89dc8..b1420b0e 100644 --- a/lib/blueprinter/v2/reflection.rb +++ b/lib/blueprinter/v2/reflection.rb @@ -4,7 +4,7 @@ require 'blueprinter/v2/field' module Blueprinter - class V2 + module V2 # API for reflecting on Blueprints module Reflection # @@ -42,7 +42,7 @@ class View attr_reader :associations - # @param blueprint [Class] A subclass of Blueprinter::V2 + # @param blueprint [Class] A subclass of Blueprinter::V2::Base # @param name [Symbol] Name of the view # @api private def initialize(blueprint, name) diff --git a/lib/blueprinter/v2/view_builder.rb b/lib/blueprinter/v2/view_builder.rb index 6a13f632..5f129aea 100644 --- a/lib/blueprinter/v2/view_builder.rb +++ b/lib/blueprinter/v2/view_builder.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Blueprinter - class V2 + module V2 # # A Hash-like class that holds a Blueprint's views, but defers evaluation of their # definitions until they're first accessed. @@ -11,7 +11,7 @@ class V2 class ViewBuilder include Enumerable - # @param parent [Class] A subclass of Blueprinter::V2 + # @param parent [Class] A subclass of Blueprinter::V2::Base def initialize(parent) @parent = parent @views = { default: parent } diff --git a/spec/v2/declarative_api_spec.rb b/spec/v2/declarative_api_spec.rb index f7b4beba..d047a883 100644 --- a/spec/v2/declarative_api_spec.rb +++ b/spec/v2/declarative_api_spec.rb @@ -2,7 +2,7 @@ describe "Blueprinter::V2 Declarative API" do it "should inherit fields defined after the view" do - blueprint = Class.new(Blueprinter::V2) do + blueprint = Class.new(Blueprinter::V2::Base) do view :desc do field :description end @@ -16,7 +16,7 @@ end it "should include partials defined after the view" do - blueprint = Class.new(Blueprinter::V2) do + blueprint = Class.new(Blueprinter::V2::Base) do field :name view :foo do @@ -33,7 +33,7 @@ end it "should include partials defined after the use statement" do - blueprint = Class.new(Blueprinter::V2) do + blueprint = Class.new(Blueprinter::V2::Base) do field :name use :desc @@ -47,7 +47,7 @@ end it "should inherit when accessing views" do - blueprint = Class.new(Blueprinter::V2) do + blueprint = Class.new(Blueprinter::V2::Base) do use :desc field :name @@ -69,7 +69,7 @@ end it "should exclude fields added after the exclude statement" do - blueprint = Class.new(Blueprinter::V2) do + blueprint = Class.new(Blueprinter::V2::Base) do field :id field :name diff --git a/spec/v2/fields_spec.rb b/spec/v2/fields_spec.rb index 6ca337a6..00dc8a8a 100644 --- a/spec/v2/fields_spec.rb +++ b/spec/v2/fields_spec.rb @@ -3,7 +3,7 @@ describe "Blueprinter::V2 Fields" do context "fields" do it "should add fields with options" do - blueprint = Class.new(Blueprinter::V2) do + blueprint = Class.new(Blueprinter::V2::Base) do field :name field :description, from: :desc, if: -> { true } field(:foo) { "foo" } @@ -23,9 +23,9 @@ context "associations" do it "should add associations with options" do - category_blueprint = Class.new(Blueprinter::V2) - widget_blueprint = Class.new(Blueprinter::V2) - blueprint = Class.new(Blueprinter::V2) do + category_blueprint = Class.new(Blueprinter::V2::Base) + widget_blueprint = Class.new(Blueprinter::V2::Base) + blueprint = Class.new(Blueprinter::V2::Base) do association :category, category_blueprint association :widgets, widget_blueprint, from: :foo, if: -> { true } association(:foo, widget_blueprint) { {foo: "bar"} } @@ -47,7 +47,7 @@ end it "it should inherit from parent classes" do - application_blueprint = Class.new(Blueprinter::V2) do + application_blueprint = Class.new(Blueprinter::V2::Base) do field :id end blueprint = Class.new(application_blueprint) do @@ -59,7 +59,7 @@ end it "it should inherit from parent views" do - blueprint = Class.new(Blueprinter::V2) do + blueprint = Class.new(Blueprinter::V2::Base) do field :name view :extended do @@ -78,7 +78,7 @@ end it "should exclude specified fields and associations from the parent class" do - application_blueprint = Class.new(Blueprinter::V2) do + application_blueprint = Class.new(Blueprinter::V2::Base) do field :id field :foo end @@ -92,9 +92,9 @@ end it "should exclude specified fields and associations from the parent view" do - category_blueprint = Class.new(Blueprinter::V2) - widget_blueprint = Class.new(Blueprinter::V2) - blueprint = Class.new(Blueprinter::V2) do + category_blueprint = Class.new(Blueprinter::V2::Base) + widget_blueprint = Class.new(Blueprinter::V2::Base) + blueprint = Class.new(Blueprinter::V2::Base) do field :id field :name association :category, category_blueprint @@ -114,7 +114,7 @@ end it "should exclude specified fields and associations from partials" do - blueprint = Class.new(Blueprinter::V2) do + blueprint = Class.new(Blueprinter::V2::Base) do partial :desc do field :short_desc field :long_desc diff --git a/spec/v2/name_spec.rb b/spec/v2/name_spec.rb index 1fc38935..cd04b532 100644 --- a/spec/v2/name_spec.rb +++ b/spec/v2/name_spec.rb @@ -2,7 +2,7 @@ describe "Blueprinter::V2 Names" do context 'const named Blueprints' do - class NamedBlueprint < Blueprinter::V2 + class NamedBlueprint < Blueprinter::V2::Base view :extended end @@ -28,7 +28,7 @@ class NamedBlueprint < Blueprinter::V2 context 'manually named Blueprints' do let(:blueprint) do - Class.new(Blueprinter::V2) do + Class.new(Blueprinter::V2::Base) do self.blueprint_name = "MyBlueprint" view :extended end @@ -47,23 +47,23 @@ class NamedBlueprint < Blueprinter::V2 context 'anonymous Blueprints' do let(:blueprint) do - Class.new(Blueprinter::V2) do + Class.new(Blueprinter::V2::Base) do view :extended end end it 'should have no base name' do - expect(blueprint.blueprint_name).to eq "Blueprinter::V2" + expect(blueprint.blueprint_name).to eq "Blueprinter::V2::Base" end it 'should find a view by name' do - expect(blueprint[:extended].blueprint_name).to eq "Blueprinter::V2.extended" + expect(blueprint[:extended].blueprint_name).to eq "Blueprinter::V2::Base.extended" end end context 'deeply nested Blueprints' do let(:blueprint) do - Class.new(Blueprinter::V2) do + Class.new(Blueprinter::V2::Base) do self.blueprint_name = "MyBlueprint" view :foo do @@ -94,7 +94,7 @@ class NamedBlueprint < Blueprinter::V2 end it "should not contain periods" do - blueprint = Class.new(Blueprinter::V2) + blueprint = Class.new(Blueprinter::V2::Base) expect { blueprint.view :"foo.bar" }.to raise_error( Blueprinter::Errors::InvalidBlueprint, /name may not contain/ diff --git a/spec/v2/partials_spec.rb b/spec/v2/partials_spec.rb index 4fc20ee0..98dcf391 100644 --- a/spec/v2/partials_spec.rb +++ b/spec/v2/partials_spec.rb @@ -2,7 +2,7 @@ describe "Blueprinter::V2 Partials" do it "should allow a partial to be used in any view" do - blueprint = Class.new(Blueprinter::V2) do + blueprint = Class.new(Blueprinter::V2::Base) do field :name partial :description do @@ -25,7 +25,7 @@ end it "should allow use statements to be nested" do - blueprint = Class.new(Blueprinter::V2) do + blueprint = Class.new(Blueprinter::V2::Base) do field :name use :foo @@ -49,7 +49,7 @@ end it "should allow a view to be defined in a partial" do - blueprint = Class.new(Blueprinter::V2) do + blueprint = Class.new(Blueprinter::V2::Base) do field :name view :foo do @@ -77,7 +77,7 @@ end it "should throw an error for an invalid partial name" do - blueprint = Class.new(Blueprinter::V2) do + blueprint = Class.new(Blueprinter::V2::Base) do view :foo do use :description end diff --git a/spec/v2/reflection_spec.rb b/spec/v2/reflection_spec.rb index eb9416c5..68f9f506 100644 --- a/spec/v2/reflection_spec.rb +++ b/spec/v2/reflection_spec.rb @@ -2,7 +2,7 @@ describe "Blueprinter::V2::Reflection" do let(:blueprint) do - Class.new(Blueprinter::V2) do + Class.new(Blueprinter::V2::Base) do view :foo view :bar do view :foo do @@ -65,9 +65,9 @@ end it "should find fields and associations" do - category_blueprint = Class.new(Blueprinter::V2) - widget_blueprint = Class.new(Blueprinter::V2) - blueprint = Class.new(Blueprinter::V2) do + category_blueprint = Class.new(Blueprinter::V2::Base) + widget_blueprint = Class.new(Blueprinter::V2::Base) + blueprint = Class.new(Blueprinter::V2::Base) do field :name association :category, category_blueprint diff --git a/spec/v2/view_builder_spec.rb b/spec/v2/view_builder_spec.rb index f87139d7..66bdd8d6 100644 --- a/spec/v2/view_builder_spec.rb +++ b/spec/v2/view_builder_spec.rb @@ -6,7 +6,7 @@ end let(:blueprint) do - Class.new(Blueprinter::V2) do + Class.new(Blueprinter::V2::Base) do field :id field :name end