diff --git a/lib/blueprinter.rb b/lib/blueprinter.rb index 7c1f1e74..bf0c399c 100644 --- a/lib/blueprinter.rb +++ b/lib/blueprinter.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative 'blueprinter/base' +require_relative 'blueprinter/extension' module Blueprinter end diff --git a/lib/blueprinter/base.rb b/lib/blueprinter/base.rb index 57663d24..3b2e5c7c 100644 --- a/lib/blueprinter/base.rb +++ b/lib/blueprinter/base.rb @@ -258,6 +258,7 @@ def self.render_as_json(object, options = {}) def self.prepare(object, view_name:, local_options:, root: nil, meta: nil) raise BlueprinterError, "View '#{view_name}' is not defined" unless view_collection.view? view_name + object = Blueprinter.configuration.extensions.pre_render(object, self, view_name, local_options) data = prepare_data(object, view_name, local_options) prepend_root_and_meta(data, root, meta) end diff --git a/lib/blueprinter/configuration.rb b/lib/blueprinter/configuration.rb index 050d07a9..04ef91fa 100644 --- a/lib/blueprinter/configuration.rb +++ b/lib/blueprinter/configuration.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative 'extensions' + module Blueprinter class Configuration attr_accessor :association_default, :datetime_format, :deprecations, :field_default, :generator, :if, :method, @@ -22,6 +24,14 @@ def initialize @custom_array_like_classes = [] end + def extensions + @extensions ||= Extensions.new + end + + def extensions=(list) + @extensions = Extensions.new(list) + end + def array_like_classes @array_like_classes ||= [ Array, diff --git a/lib/blueprinter/extension.rb b/lib/blueprinter/extension.rb new file mode 100644 index 00000000..2d4059ec --- /dev/null +++ b/lib/blueprinter/extension.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Blueprinter + # + # Base class for all extensions. All extension methods are implemented as no-ops. + # + class Extension + # + # Called eary during "render", this method receives the object to be rendered and + # may return a modified (or new) object to be rendered. + # + # @param object [Object] The object to be rendered + # @param _blueprint [Class] The Blueprinter class + # @param _view [Symbol] The blueprint view + # @param _options [Hash] Options passed to "render" + # @return [Object] The object to continue rendering + # + def pre_render(object, _blueprint, _view, _options) + object + end + end +end diff --git a/lib/blueprinter/extensions.rb b/lib/blueprinter/extensions.rb new file mode 100644 index 00000000..5b49209f --- /dev/null +++ b/lib/blueprinter/extensions.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Blueprinter + # + # Stores and runs Blueprinter extensions. An extension is any object that implements one or more of the + # extension methods: + # + # The Render Extension intercepts an object before rendering begins. The return value from this + # method is what is ultimately rendered. + # + # def pre_render(object, blueprint, view, options) + # # returns original, modified, or new object + # end + # + class Extensions + def initialize(extensions = []) + @extensions = extensions + end + + def to_a + @extensions.dup + end + + # Appends an extension + def <<(ext) + @extensions << ext + self + end + + # Runs the object through all Render Extensions and returns the final result + def pre_render(object, blueprint, view, options = {}) + @extensions.reduce(object) do |acc, ext| + ext.pre_render(acc, blueprint, view, options) + end + end + end +end diff --git a/spec/units/extensions_spec.rb b/spec/units/extensions_spec.rb new file mode 100644 index 00000000..9065091b --- /dev/null +++ b/spec/units/extensions_spec.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'json' +require 'ostruct' + +describe Blueprinter::Extensions do + let(:all_extensions) { + [ + foo_extension.new, + bar_extension.new, + zar_extension.new, + ] + } + + let(:foo_extension) { + Class.new(Blueprinter::Extension) do + def pre_render(object, _blueprint, _view, _options) + obj = object.dup + obj.foo = "Foo" + obj + end + end + } + + let(:bar_extension) { + Class.new(Blueprinter::Extension) do + def pre_render(object, _blueprint, _view, _options) + obj = object.dup + obj.bar = "Bar" + obj + end + end + } + + let(:zar_extension) { + Class.new(Blueprinter::Extension) do + def self.something_else(object, _blueprint, _view, _options) + object + end + end + } + + it 'should append extensions' do + extensions = Blueprinter::Extensions.new + extensions << foo_extension.new + extensions << bar_extension.new + extensions << zar_extension.new + expect(extensions.to_a.map(&:class)).to eq [ + foo_extension, + bar_extension, + zar_extension, + ] + end + + it "should initialize with extensions, removing any that don't have recognized extension methods" do + extensions = Blueprinter::Extensions.new(all_extensions) + expect(extensions.to_a.map(&:class)).to eq [ + foo_extension, + bar_extension, + zar_extension, + ] + end + + context '#pre_render' do + before :each do + Blueprinter.configure do |config| + config.extensions = all_extensions + end + end + + after :each do + Blueprinter.configure do |config| + config.extensions = [] + end + end + + let(:test_blueprint) { + Class.new(Blueprinter::Base) do + field :id + field :name + field :foo + + view :with_bar do + field :bar + end + end + } + + it 'should run all pre_render extensions' do + extensions = Blueprinter::Extensions.new(all_extensions) + obj = OpenStruct.new(id: 42, name: 'Jack') + obj = extensions.pre_render(obj, test_blueprint, :default, {}) + expect(obj.id).to be 42 + expect(obj.name).to eq 'Jack' + expect(obj.foo).to eq 'Foo' + expect(obj.bar).to eq 'Bar' + end + + it 'should run with Blueprinter.render using default view' do + obj = OpenStruct.new(id: 42, name: 'Jack') + res = JSON.parse(test_blueprint.render(obj)) + expect(res['id']).to be 42 + expect(res['name']).to eq 'Jack' + expect(res['foo']).to eq 'Foo' + expect(res['bar']).to be_nil + end + + it 'should run with Blueprinter.render using with_bar view' do + obj = OpenStruct.new(id: 42, name: 'Jack') + res = JSON.parse(test_blueprint.render(obj, view: :with_bar)) + expect(res['id']).to be 42 + expect(res['name']).to eq 'Jack' + expect(res['foo']).to eq 'Foo' + expect(res['bar']).to eq 'Bar' + end + + it 'should run with Blueprinter.render_as_hash' do + obj = OpenStruct.new(id: 42, name: 'Jack') + res = test_blueprint.render_as_hash(obj, view: :with_bar) + expect(res[:id]).to be 42 + expect(res[:name]).to eq 'Jack' + expect(res[:foo]).to eq 'Foo' + expect(res[:bar]).to eq 'Bar' + end + end +end