Skip to content

Commit

Permalink
Break V2 into modules. Start on reflection
Browse files Browse the repository at this point in the history
Signed-off-by: Jordan Hollinger <[email protected]>
  • Loading branch information
jhollinger committed Aug 6, 2024
1 parent ee90379 commit 7af7bae
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 64 deletions.
73 changes: 26 additions & 47 deletions lib/blueprinter/v2.rb
Original file line number Diff line number Diff line change
@@ -1,36 +1,53 @@
# frozen_string_literal: true

require 'blueprinter/v2/dsl'
require 'blueprinter/v2/options'
require 'blueprinter/v2/reflection'

module Blueprinter
class V2
autoload :Options, 'blueprinter/v2/options'
extend DSL
extend Reflection

class << self
attr_accessor :views, :fields, :extensions, :options, :blueprint_name
attr_accessor :views, :fields, :extensions, :options
# The fully-qualified name, e.g. "MyBlueprint", or "MyBlueprint.foo.bar"
attr_accessor :blueprint_name
# Name of the view, e.g. :default, :foo, :"foo.bar"
attr_accessor :view_name
end

self.views = {}
self.fields = {}
self.extensions = []
self.options = Options::Options.new(Options::DEFAULTS)
self.blueprint_name = []
self.blueprint_name = name
self.view_name = :default

# Initialize subclass
def self.inherited(subclass)
subclass.views = { default: subclass }
subclass.fields = fields.dup
subclass.fields = fields.transform_values(&:dup)
subclass.extensions = extensions.dup
subclass.options = options.dup
subclass.blueprint_name = subclass.name ? [subclass.name] : blueprint_name.dup
subclass.blueprint_name = subclass.name || blueprint_name
subclass.view_name = subclass.name ? :default : view_name
end

# A descriptive name for the Blueprint view, e.g. "WidgetBlueprint:extended"
# A descriptive name for the Blueprint view, e.g. "WidgetBlueprint.extended"
def self.inspect
to_s
blueprint_name
end

# A descriptive name for the Blueprint view, e.g. "WidgetBlueprint:extended"
# A descriptive name for the Blueprint view, e.g. "WidgetBlueprint.extended"
def self.to_s
blueprint_name.join '.'
blueprint_name
end

# Append the sub-view name to blueprint_name and view_name
def self.append_name(name)
self.blueprint_name = "#{blueprint_name}.#{name}"
self.view_name = view_name == :default ? name : :"#{view_name}.#{name}"
end

# Access a child view
Expand All @@ -44,42 +61,6 @@ def self.[](view)
end
end

# Define a new child view, which is a subclass of self
def self.view(name, &definition)
raise Errors::InvalidBlueprint, "View name may not contain '.'" if name.to_s =~ /\./

name = name.to_sym
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.to_sym] = 'TODO'
end

# Define an association
def self.association(name, blueprint, options = {})
fields[name.to_sym] = 'TODO'
end

# Exclude fields/associations
def self.exclude(*names)
unknown = []
names.each do |name|
name_sym = name.to_sym
if fields.key? name_sym
fields.delete name_sym
else
unknown << name.to_s
end
end
raise Errors::InvalidBlueprint, "Unknown excluded fields in '#{self}': #{unknown.join(', ')}" if unknown.any?
end

def self.render(obj, options = {})
new.render(obj, options)
end
Expand All @@ -95,7 +76,5 @@ def render(obj, options = {})
# 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

# rubocop:enable Lint/UnusedMethodArgument
end
end
44 changes: 44 additions & 0 deletions lib/blueprinter/v2/dsl.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

module Blueprinter
class V2
module DSL
# Define a new child view, which is a subclass of self
def view(name, &definition)
raise Errors::InvalidBlueprint, "View name may not contain '.'" if name.to_s =~ /\./

view = Class.new(self)
view.append_name(name)
view.class_eval(&definition) if definition
views[name.to_sym] = view
end

# Define a field
# rubocop:todo Lint/UnusedMethodArgument
def field(name, options = {})
fields[name.to_sym] = 'TODO'
end

# Define an association
def association(name, blueprint, options = {})
fields[name.to_sym] = 'TODO'
end

# Exclude fields/associations
def exclude(*names)
unknown = []
names.each do |name|
name_sym = name.to_sym
if fields.key? name_sym
fields.delete name_sym
else
unknown << name.to_s
end
end
raise Errors::InvalidBlueprint, "Unknown excluded fields in '#{self}': #{unknown.join(', ')}" if unknown.any?
end

# rubocop:enable Lint/UnusedMethodArgument
end
end
end
49 changes: 49 additions & 0 deletions lib/blueprinter/v2/reflection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

module Blueprinter
class V2
module Reflection
def self.extended(klass)
klass.class_eval do
private_class_method :flattened_views
end
end

#
# Returns a Hash of views keyed by name.
#
# @return [Hash<Symbol, Blueprinter::V2::Reflection::View>]
#
def reflections
@reflections ||= flattened_views(views)
end

# Builds a flat Hash of nested views
def flattened_views(views, acc = {})
views.each_with_object(acc) do |(_, blueprint), obj|
obj[blueprint.view_name] = View.new(blueprint)
children = blueprint.views.except(:default)
flattened_views(children, obj)
end
end

#
# Represents a view within a Blueprint.
#
class View
# @return [Symbol] Name of the view
attr_reader :name
# @return [Hash<Symbol, TODO>] Fields defined on the view
attr_reader :fields
# @return [Hash<Symbol, TODO>] Associations defined on the view
attr_reader :associations

def initialize(blueprint)
@name = blueprint.view_name
@fields = {} # TODO: get non-association fields from blueprint.fields
@associations = {} # TODO: get association fields from blueprint.fields
end
end
end
end
end
45 changes: 28 additions & 17 deletions spec/v2/name_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ class NamedBlueprint < Blueprinter::V2
it 'should have a base name' do
expect(NamedBlueprint.to_s).to eq "NamedBlueprint"
expect(NamedBlueprint.inspect).to eq "NamedBlueprint"
expect(NamedBlueprint.blueprint_name).to eq "NamedBlueprint"
expect(NamedBlueprint.view_name).to eq :default
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"
expect(NamedBlueprint[:extended].blueprint_name).to eq "NamedBlueprint.extended"
expect(NamedBlueprint[:extended].view_name).to eq :extended
end

it 'should raise for an invalid view name' do
Expand All @@ -27,19 +31,21 @@ class NamedBlueprint < Blueprinter::V2
context 'manually named Blueprints' do
let(:blueprint) do
Class.new(Blueprinter::V2) do
blueprint_name << "MyBlueprint"
self.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"
expect(blueprint.view_name).to eq :default
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"
expect(blueprint[:extended].view_name).to eq :extended
end
end

Expand All @@ -51,20 +57,20 @@ class NamedBlueprint < Blueprinter::V2
end

it 'should have no base name' do
expect(blueprint.to_s).to eq ""
expect(blueprint.inspect).to eq ""
expect(blueprint.blueprint_name).to eq "Blueprinter::V2"
expect(blueprint.view_name).to eq :default
end

it 'should find a view by name' do
expect(blueprint[:extended].to_s).to eq "extended"
expect(blueprint[:extended].inspect).to eq "extended"
expect(blueprint[:extended].blueprint_name).to eq "Blueprinter::V2.extended"
expect(blueprint[:extended].view_name).to eq :extended
end
end

context 'deeply nested Blueprints' do
let(:blueprint) do
Class.new(Blueprinter::V2) do
blueprint_name << "MyBlueprint"
self.blueprint_name = "MyBlueprint"

view :foo do
view :bar do
Expand All @@ -75,23 +81,28 @@ class NamedBlueprint < Blueprinter::V2
end

it 'should find deeply nested names' do
expect(blueprint.to_s).to eq "MyBlueprint"
expect(blueprint.inspect).to eq "MyBlueprint"
expect(blueprint.blueprint_name).to eq "MyBlueprint"
expect(blueprint.view_name).to eq :default

expect(blueprint[:foo].to_s).to eq "MyBlueprint.foo"
expect(blueprint[:foo].inspect).to eq "MyBlueprint.foo"
expect(blueprint[:foo].blueprint_name).to eq "MyBlueprint.foo"
expect(blueprint[:foo].view_name).to eq :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].blueprint_name).to eq "MyBlueprint.foo.bar"
expect(blueprint[:foo][:bar].view_name).to eq :"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"
expect(blueprint[:foo][:bar][:zorp].blueprint_name).to eq "MyBlueprint.foo.bar.zorp"
expect(blueprint[:foo][:bar][:zorp].view_name).to eq :"foo.bar.zorp"
end

it 'should find deeply nested names using dot syntax' do
expect(blueprint["foo"].to_s).to eq "MyBlueprint.foo"
expect(blueprint["foo.bar"].to_s).to eq "MyBlueprint.foo.bar"
expect(blueprint["foo.bar.zorp"].to_s).to eq "MyBlueprint.foo.bar.zorp"
expect(blueprint["foo"].blueprint_name).to eq "MyBlueprint.foo"
expect(blueprint["foo"].view_name).to eq :foo

expect(blueprint["foo.bar"].blueprint_name).to eq "MyBlueprint.foo.bar"
expect(blueprint["foo.bar"].view_name).to eq :"foo.bar"

expect(blueprint["foo.bar.zorp"].blueprint_name).to eq "MyBlueprint.foo.bar.zorp"
expect(blueprint["foo.bar.zorp"].view_name).to eq :"foo.bar.zorp"
end
end

Expand Down
23 changes: 23 additions & 0 deletions spec/v2/reflection_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

describe "Blueprinter::V2 Reflection" do
it "should find all view names" do
blueprint = Class.new(Blueprinter::V2) do
view :foo
view :bar do
view :foo do
view :borp
end
end
end

view_names = blueprint.reflections.keys
expect(view_names.sort).to eq %i(
default
foo
bar
bar.foo
bar.foo.borp
).sort
end
end

0 comments on commit 7af7bae

Please sign in to comment.