diff --git a/lib/blueprinter/base.rb b/lib/blueprinter/base.rb index acb4182e..57663d24 100644 --- a/lib/blueprinter/base.rb +++ b/lib/blueprinter/base.rb @@ -17,10 +17,12 @@ require_relative 'view' require_relative 'view_collection' require_relative 'transformer' +require_relative 'reflection' module Blueprinter class Base include BaseHelpers + extend Reflection # Specify a field or method name used as an identifier. Usually, this is # something like :id diff --git a/lib/blueprinter/reflection.rb b/lib/blueprinter/reflection.rb new file mode 100644 index 00000000..e3ce4216 --- /dev/null +++ b/lib/blueprinter/reflection.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module Blueprinter + # + # Public methods for reflecting on a Blueprint. + # + module Reflection + Field = Struct.new(:name, :display_name, :options) + Association = Struct.new(:name, :display_name, :blueprint, :view, :options) + + # + # Returns a Hash of views keyed by name. + # + # Example: + # + # widget_view = WidgetBlueprint.reflections[:default] + # category = widget_view.associations[:category] + # category.blueprint + # => CategoryBlueprint + # category.view + # => :default + # + # @return [Hash] + # + def reflections + @reflections ||= view_collection.views.transform_values do |view| + View.new(view.name, view_collection) + end + end + + # + # Represents a view within a Blueprint. + # + class View + attr_reader :name + + def initialize(name, view_collection) + @name = name + @view_collection = view_collection + end + + # + # Returns a Hash of fields in this view (recursive) keyed by method name. + # + # @return [Hash] + # + def fields + @fields ||= @view_collection.fields_for(name).each_with_object({}) do |field, obj| + next if field.options[:association] + + obj[field.method] = Field.new(field.method, field.name, field.options) + end + end + + # + # Returns a Hash of associations in this view (recursive) keyed by method name. + # + # @return [Hash] + # + def associations + @associations ||= @view_collection.fields_for(name).each_with_object({}) do |field, obj| + next unless field.options[:association] + + blueprint = field.options.fetch(:blueprint) + view = field.options[:view] || :default + obj[field.method] = Association.new(field.method, field.name, blueprint, view, field.options) + end + end + end + end +end diff --git a/spec/units/reflection_spec.rb b/spec/units/reflection_spec.rb new file mode 100644 index 00000000..86135068 --- /dev/null +++ b/spec/units/reflection_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'json' + +describe Blueprinter::Reflection do + let(:category_blueprint) { + Class.new(Blueprinter::Base) do + fields :id, :name + end + } + + let(:part_blueprint) { + Class.new(Blueprinter::Base) do + fields :id, :name + + view :extended do + field :description + end + end + } + + let(:widget_blueprint) { + cat_bp = category_blueprint + part_bp = part_blueprint + Class.new(Blueprinter::Base) do + fields :id, :name + association :category, blueprint: cat_bp + + view :extended do + association :parts, blueprint: part_bp, view: :extended + end + + view :extended_plus do + include_view :extended + field :foo + association :foos, blueprint: part_bp + end + + view :extended_plus_plus do + include_view :extended_plus + field :bar + association :bars, blueprint: part_bp + end + + view :legacy do + association :parts, blueprint: part_bp, name: :pieces + end + end + } + + it 'should list views' do + expect(widget_blueprint.reflections.keys.sort).to eq [ + :identifier, + :default, + :extended, + :extended_plus, + :extended_plus_plus, + :legacy, + ].sort + end + + it 'should list fields' do + expect(part_blueprint.reflections.fetch(:extended).fields.keys.sort).to eq [ + :id, + :name, + :description, + ].sort + end + + it 'should list fields from included views' do + expect(widget_blueprint.reflections.fetch(:extended_plus_plus).fields.keys.sort).to eq [ + :id, + :name, + :foo, + :bar, + ].sort + end + + it 'should list associations' do + associations = widget_blueprint.reflections.fetch(:default).associations + expect(associations.keys).to eq [:category] + end + + it 'should list associations from included views' do + associations = widget_blueprint.reflections.fetch(:extended_plus_plus).associations + expect(associations.keys.sort).to eq [:category, :parts, :foos, :bars].sort + end + + it 'should list associations using custom names' do + associations = widget_blueprint.reflections.fetch(:legacy).associations + expect(associations.keys).to eq [:category, :parts] + expect(associations[:parts].display_name).to eq :pieces + end + + it 'should get a blueprint and view from an association' do + assoc = widget_blueprint.reflections[:extended].associations[:parts] + expect(assoc.name).to eq :parts + expect(assoc.display_name).to eq :parts + expect(assoc.blueprint).to eq part_blueprint + expect(assoc.view).to eq :extended + end +end