From ef6bd58f0a92263c49b355247a296887f8e20f48 Mon Sep 17 00:00:00 2001 From: Jordan Hollinger Date: Wed, 8 Nov 2023 15:29:34 -0500 Subject: [PATCH 1/8] Add extensions with initial support for pre_render Signed-off-by: Jordan Hollinger --- lib/blueprinter/base.rb | 1 + lib/blueprinter/configuration.rb | 10 +++ lib/blueprinter/extensions.rb | 38 ++++++++++ spec/units/extensions_spec.rb | 123 +++++++++++++++++++++++++++++++ 4 files changed, 172 insertions(+) create mode 100644 lib/blueprinter/extensions.rb create mode 100644 spec/units/extensions_spec.rb diff --git a/lib/blueprinter/base.rb b/lib/blueprinter/base.rb index acb4182e..be6c7fdc 100644 --- a/lib/blueprinter/base.rb +++ b/lib/blueprinter/base.rb @@ -256,6 +256,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/extensions.rb b/lib/blueprinter/extensions.rb new file mode 100644 index 00000000..0f3ebf3e --- /dev/null +++ b/lib/blueprinter/extensions.rb @@ -0,0 +1,38 @@ +# 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 = []) + @pre_renderers = [] + extensions.each { |ext| self << ext } + end + + def to_a + @pre_renderers.uniq + end + + # Appends an extension + def <<(ext) + @pre_renderers << ext if ext.respond_to? :pre_render + self + end + + # Runs the object through all Render Extensions and returns the final result + def pre_render(object, blueprint, view, options = {}) + @pre_renderers.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..fcdb9fb8 --- /dev/null +++ b/spec/units/extensions_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'json' +require 'ostruct' + +describe Blueprinter::Extensions do + let(:all_extensions) { + [ + foo_extension, + bar_extension, + zar_extension, + ] + } + + let(:foo_extension) { + Module.new do + def self.pre_render(object, _blueprint, _view, _options) + obj = object.dup + obj.foo = "Foo" + obj + end + end + } + + let(:bar_extension) { + Module.new do + def self.pre_render(object, _blueprint, _view, _options) + obj = object.dup + obj.bar = "Bar" + obj + end + end + } + + let(:zar_extension) { + Module.new do + def self.something_else(object, _blueprint, _view, _options) + object + end + end + } + + it 'should append extensions' do + extensions = Blueprinter::Extensions.new + extensions << foo_extension + extensions << bar_extension + extensions << zar_extension + expect(extensions.to_a).to eq [ + foo_extension, + bar_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).to eq [ + foo_extension, + bar_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| + 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 From 342fc0d7ebd5eb1197aa8c3ebeb59a7a2ed1976e Mon Sep 17 00:00:00 2001 From: Jordan Hollinger Date: Mon, 27 Nov 2023 09:04:22 -0500 Subject: [PATCH 2/8] Blueprinter::Extension base class Signed-off-by: Jordan Hollinger --- lib/blueprinter.rb | 1 + lib/blueprinter/extension.rb | 22 ++++++++++++++++++++++ lib/blueprinter/extensions.rb | 9 ++++----- spec/units/extensions_spec.rb | 28 +++++++++++++++------------- 4 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 lib/blueprinter/extension.rb 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/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 index 0f3ebf3e..5b49209f 100644 --- a/lib/blueprinter/extensions.rb +++ b/lib/blueprinter/extensions.rb @@ -14,23 +14,22 @@ module Blueprinter # class Extensions def initialize(extensions = []) - @pre_renderers = [] - extensions.each { |ext| self << ext } + @extensions = extensions end def to_a - @pre_renderers.uniq + @extensions.dup end # Appends an extension def <<(ext) - @pre_renderers << ext if ext.respond_to? :pre_render + @extensions << ext self end # Runs the object through all Render Extensions and returns the final result def pre_render(object, blueprint, view, options = {}) - @pre_renderers.reduce(object) do |acc, ext| + @extensions.reduce(object) do |acc, ext| ext.pre_render(acc, blueprint, view, options) end end diff --git a/spec/units/extensions_spec.rb b/spec/units/extensions_spec.rb index fcdb9fb8..7a891e1e 100644 --- a/spec/units/extensions_spec.rb +++ b/spec/units/extensions_spec.rb @@ -6,15 +6,15 @@ describe Blueprinter::Extensions do let(:all_extensions) { [ - foo_extension, - bar_extension, - zar_extension, + foo_extension.new, + bar_extension.new, + zar_extension.new, ] } let(:foo_extension) { - Module.new do - def self.pre_render(object, _blueprint, _view, _options) + Class.new(Blueprinter::Extension) do + def pre_render(object, _blueprint, _view, _options) obj = object.dup obj.foo = "Foo" obj @@ -23,8 +23,8 @@ def self.pre_render(object, _blueprint, _view, _options) } let(:bar_extension) { - Module.new do - def self.pre_render(object, _blueprint, _view, _options) + Class.new(Blueprinter::Extension) do + def pre_render(object, _blueprint, _view, _options) obj = object.dup obj.bar = "Bar" obj @@ -33,7 +33,7 @@ def self.pre_render(object, _blueprint, _view, _options) } let(:zar_extension) { - Module.new do + Class.new(Blueprinter::Extension) do def self.something_else(object, _blueprint, _view, _options) object end @@ -42,20 +42,22 @@ def self.something_else(object, _blueprint, _view, _options) it 'should append extensions' do extensions = Blueprinter::Extensions.new - extensions << foo_extension - extensions << bar_extension - extensions << zar_extension - expect(extensions.to_a).to eq [ + 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).to eq [ + expect(extensions.to_a.map(&:class)).to eq [ foo_extension, bar_extension, + zar_extension, ] end From 01f191a29c8dbea43ca97a94f5899d6d6855a113 Mon Sep 17 00:00:00 2001 From: Jordan Hollinger Date: Thu, 4 Jan 2024 09:10:17 -0500 Subject: [PATCH 3/8] Clear extensions after each test Signed-off-by: Jordan Hollinger --- spec/units/extensions_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/units/extensions_spec.rb b/spec/units/extensions_spec.rb index 7a891e1e..9065091b 100644 --- a/spec/units/extensions_spec.rb +++ b/spec/units/extensions_spec.rb @@ -70,6 +70,7 @@ def self.something_else(object, _blueprint, _view, _options) after :each do Blueprinter.configure do |config| + config.extensions = [] end end From 9d7690f7394928600d1ecde2092964e4931827c6 Mon Sep 17 00:00:00 2001 From: Jordan Hollinger Date: Tue, 9 Jan 2024 16:18:19 -0500 Subject: [PATCH 4/8] Force checks to run Signed-off-by: Jordan Hollinger From 96ecfed00043a192256b2aff123db1dd577d12ef Mon Sep 17 00:00:00 2001 From: Nate Baer Date: Tue, 16 Jan 2024 10:26:43 -0800 Subject: [PATCH 5/8] Create 0.31.0 release. Signed-off-by: Nate Baer --- CHANGELOG.md | 4 ++++ lib/blueprinter/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14ff8a5e..f26ed3b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.31.0 - 2024/01/16 +* 🚀 [BREAKING] Allow transformers to be included across views. See [this](https://github.com/procore-oss/blueprinter#transform-across-views) section of the README for details, also PR [#372](https://github.com/procore-oss/blueprinter/pull/372) and issue [#225](https://github.com/procore-oss/blueprinter/issues/225). Note this changes the behavior of transformers which were previously only applied to the view they were defined on. Thanks to [@njbbaer](https://github.com/njbbaer) and [@bhooshiek-narendiran](https://github.com/bhooshiek-narendiran). +* 💅 [ENHANCEMENT] Add reflection on views, fields, and associations. See PR [#357](https://github.com/procore-oss/blueprinter/pull/357) and issue [#341](https://github.com/procore-oss/blueprinter/issues/341). Thanks to [@jhollinger](https://github.com/jhollinger). + ## 0.30.0 - 2023/09/16 * 🚀 [FEATURE] Allow configuring custom array-like classes to be treated as collections when serializing. More details can be found [here](https://github.com/procore-oss/blueprinter/pull/327). Thanks to [@toddnestor](https://github.com/toddnestor). * 💅 [ENHANCEMENT] Reduce object allocations in fields calculations to save some memory. More details can be found [here](https://github.com/procore-oss/blueprinter/pull/327). Thanks to [@nametoolong](https://github.com/nametoolong). diff --git a/lib/blueprinter/version.rb b/lib/blueprinter/version.rb index 2e640cbc..7c38152c 100644 --- a/lib/blueprinter/version.rb +++ b/lib/blueprinter/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Blueprinter - VERSION = '0.30.0' + VERSION = '0.31.0' end From 3e93bb23e3cbd8c014c0917c4b8a79a59405b435 Mon Sep 17 00:00:00 2001 From: Nate Baer Date: Tue, 16 Jan 2024 10:32:50 -0800 Subject: [PATCH 6/8] Fixes mistake in transform classes documentation. Resolves [#282](https://github.com/procore-oss/blueprinter/issues/282). Signed-off-by: Nate Baer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dde177aa..e752a6b3 100644 --- a/README.md +++ b/README.md @@ -765,7 +765,7 @@ processing and transforming of resulting view field hashes prior to serializatio Use `transform` to specify one transformer to be included for serialization. A transformer is a class, extending `Blueprinter::Transformer` and implementing the `transform` method. -Whatever is returned from this `transform` method will end up being the resulting hash passed to serialization. +The modified `hash` object will be the resulting hash passed to serialization. #### Example From 0aa0b4c94a43ae28a38ff2a11f5208cd71cf6aff Mon Sep 17 00:00:00 2001 From: Nate Baer Date: Wed, 17 Jan 2024 13:57:19 -0800 Subject: [PATCH 7/8] Switch to version 1.0.0 Signed-off-by: Nate Baer --- CHANGELOG.md | 4 ++-- lib/blueprinter/version.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f26ed3b2..00fc2f2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ -## 0.31.0 - 2024/01/16 +## 1.0.0 - 2024/01/17 * 🚀 [BREAKING] Allow transformers to be included across views. See [this](https://github.com/procore-oss/blueprinter#transform-across-views) section of the README for details, also PR [#372](https://github.com/procore-oss/blueprinter/pull/372) and issue [#225](https://github.com/procore-oss/blueprinter/issues/225). Note this changes the behavior of transformers which were previously only applied to the view they were defined on. Thanks to [@njbbaer](https://github.com/njbbaer) and [@bhooshiek-narendiran](https://github.com/bhooshiek-narendiran). -* 💅 [ENHANCEMENT] Add reflection on views, fields, and associations. See PR [#357](https://github.com/procore-oss/blueprinter/pull/357) and issue [#341](https://github.com/procore-oss/blueprinter/issues/341). Thanks to [@jhollinger](https://github.com/jhollinger). +* 💅 [ENHANCEMENT] Add reflection on views, fields, and associations. See PRs [#357](https://github.com/procore-oss/blueprinter/pull/357), [#358](https://github.com/procore-oss/blueprinter/pull/358), and issue [#341](https://github.com/procore-oss/blueprinter/issues/341). Thanks to [@jhollinger](https://github.com/jhollinger). ## 0.30.0 - 2023/09/16 * 🚀 [FEATURE] Allow configuring custom array-like classes to be treated as collections when serializing. More details can be found [here](https://github.com/procore-oss/blueprinter/pull/327). Thanks to [@toddnestor](https://github.com/toddnestor). diff --git a/lib/blueprinter/version.rb b/lib/blueprinter/version.rb index 7c38152c..1f607521 100644 --- a/lib/blueprinter/version.rb +++ b/lib/blueprinter/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Blueprinter - VERSION = '0.31.0' + VERSION = '1.0.0' end From cafff50c7d3dc5fc643366926b761ea2960e7702 Mon Sep 17 00:00:00 2001 From: Nate Baer Date: Wed, 17 Jan 2024 15:16:05 -0800 Subject: [PATCH 8/8] Update changelog with details of the new extension API. Signed-off-by: Nate Baer --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00fc2f2c..0c027572 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 1.0.0 - 2024/01/17 -* 🚀 [BREAKING] Allow transformers to be included across views. See [this](https://github.com/procore-oss/blueprinter#transform-across-views) section of the README for details, also PR [#372](https://github.com/procore-oss/blueprinter/pull/372) and issue [#225](https://github.com/procore-oss/blueprinter/issues/225). Note this changes the behavior of transformers which were previously only applied to the view they were defined on. Thanks to [@njbbaer](https://github.com/njbbaer) and [@bhooshiek-narendiran](https://github.com/bhooshiek-narendiran). -* 💅 [ENHANCEMENT] Add reflection on views, fields, and associations. See PRs [#357](https://github.com/procore-oss/blueprinter/pull/357), [#358](https://github.com/procore-oss/blueprinter/pull/358), and issue [#341](https://github.com/procore-oss/blueprinter/issues/341). Thanks to [@jhollinger](https://github.com/jhollinger). +* 🚀 [BREAKING] Allow transformers to be included across views. See [README](https://github.com/procore-oss/blueprinter#transform-across-views), PR [#372](https://github.com/procore-oss/blueprinter/pull/372) and issue [#225](https://github.com/procore-oss/blueprinter/issues/225) for details. Note this changes the behavior of transformers which were previously only applied to the view they were defined on. Thanks to [@njbbaer](https://github.com/njbbaer) and [@bhooshiek-narendiran](https://github.com/bhooshiek-narendiran). +* 🚀 [FEATURE] Introduce extension API, with initial support for pre_render hook. See [#358](https://github.com/procore-oss/blueprinter/pull/358) for details. Thanks to [@jhollinger](https://github.com/jhollinger). +* 💅 [ENHANCEMENT] Add reflection on views, fields, and associations. See PR [#357](https://github.com/procore-oss/blueprinter/pull/357), and issue [#341](https://github.com/procore-oss/blueprinter/issues/341) for details. Thanks to [@jhollinger](https://github.com/jhollinger). ## 0.30.0 - 2023/09/16 * 🚀 [FEATURE] Allow configuring custom array-like classes to be treated as collections when serializing. More details can be found [here](https://github.com/procore-oss/blueprinter/pull/327). Thanks to [@toddnestor](https://github.com/toddnestor).