From 339775a1933d8b3ece859959085f4bd82a116a6c Mon Sep 17 00:00:00 2001 From: Jordan Hollinger Date: Sun, 10 Nov 2024 22:23:10 -0500 Subject: [PATCH] Pass a Context to V2 extractor Signed-off-by: Jordan Hollinger --- lib/blueprinter/v2/extensions/values.rb | 6 ++--- lib/blueprinter/v2/extractor.rb | 23 +++++++++-------- lib/blueprinter/v2/fields.rb | 13 ++++++++++ spec/v2/extensions/extraction_spec.rb | 12 ++++----- spec/v2/extractor_spec.rb | 34 ++++++++++++++----------- 5 files changed, 54 insertions(+), 34 deletions(-) diff --git a/lib/blueprinter/v2/extensions/values.rb b/lib/blueprinter/v2/extensions/values.rb index 1752ebf8..86216b54 100644 --- a/lib/blueprinter/v2/extensions/values.rb +++ b/lib/blueprinter/v2/extensions/values.rb @@ -7,7 +7,7 @@ class Values < Extension # @param ctx [Blueprinter::V2::Context] def field_value(ctx) data = ctx.store[ctx.field.object_id] - ctx.value = data[:extractor].field(ctx.blueprint, ctx.field, ctx.object, ctx.options) + ctx.value = data[:extractor].field(ctx) default_if = data[:default_if] return ctx.value unless ctx.value.nil? || (default_if && use_default?(default_if, ctx)) @@ -18,7 +18,7 @@ def field_value(ctx) # @param ctx [Blueprinter::V2::Context] def object_value(ctx) data = ctx.store[ctx.field.object_id] - ctx.value = data[:extractor].object(ctx.blueprint, ctx.field, ctx.object, ctx.options) + ctx.value = data[:extractor].object(ctx) default_if = data[:default_if] return ctx.value unless ctx.value.nil? || (default_if && use_default?(default_if, ctx)) @@ -29,7 +29,7 @@ def object_value(ctx) # @param ctx [Blueprinter::V2::Context] def collection_value(ctx) data = ctx.store[ctx.field.object_id] - ctx.value = data[:extractor].collection(ctx.blueprint, ctx.field, ctx.object, ctx.options) + ctx.value = data[:extractor].collection(ctx) default_if = data[:default_if] return ctx.value unless ctx.value.nil? || (default_if && use_default?(default_if, ctx)) diff --git a/lib/blueprinter/v2/extractor.rb b/lib/blueprinter/v2/extractor.rb index 5402e888..251bfbb3 100644 --- a/lib/blueprinter/v2/extractor.rb +++ b/lib/blueprinter/v2/extractor.rb @@ -4,22 +4,25 @@ module Blueprinter module V2 # The default extractor and base class for custom extractors class Extractor - def field(blueprint, field, object, options) - if field.value_proc - blueprint.instance_exec(object, options, &field.value_proc) - elsif object.is_a? Hash - object[field.from] + # @param ctx [Blueprinter::V2::Context] + def field(ctx) + if ctx.field.value_proc + ctx.blueprint.instance_exec(ctx.object, ctx.options, &ctx.field.value_proc) + elsif ctx.object.is_a? Hash + ctx.object[ctx.field.from] else - object.public_send(field.from) + ctx.object.public_send(ctx.field.from) end end - def object(blueprint, field, object, options) - field(blueprint, field, object, options) + # @param ctx [Blueprinter::V2::Context] + def object(ctx) + field ctx end - def collection(blueprint, field, object, options) - field(blueprint, field, object, options) + # @param ctx [Blueprinter::V2::Context] + def collection(ctx) + field ctx end end end diff --git a/lib/blueprinter/v2/fields.rb b/lib/blueprinter/v2/fields.rb index e40ff7fb..fd83e348 100644 --- a/lib/blueprinter/v2/fields.rb +++ b/lib/blueprinter/v2/fields.rb @@ -28,6 +28,19 @@ module V2 keyword_init: true ) + # + # The Context struct is used for most extension hooks and all extractor methods. + # Some fields are always present, others are context-dependant. Each hook and extractor + # method will separately document which fields to expect and any special meanings. + # + # blueprint = Instance of the current Blueprint class (always) + # field = Field | ObjectField | Collection (optional) + # value = The current value of `field` or the Blueprint output (optional) + # object = The object currently being evaluated (e.g. passed to `render` or from an association) (optional) + # options = Options passed to `render` (always) + # instances = Allows this entire render to share instances of Blueprints and Extractors (always) + # store = A Hash to for extensions, etc to cache render data in (always) + # Context = Struct.new(:blueprint, :field, :value, :object, :options, :instances, :store) end end diff --git a/spec/v2/extensions/extraction_spec.rb b/spec/v2/extensions/extraction_spec.rb index 781f29d2..184a8d67 100644 --- a/spec/v2/extensions/extraction_spec.rb +++ b/spec/v2/extensions/extraction_spec.rb @@ -6,17 +6,17 @@ let(:object) { { foo: 'Foo', foo_obj: { name: 'Bar' }, foos: [{ num: 42 }] } } let(:my_extractor) do Class.new(Blueprinter::V2::Extractor) do - def field(_blueprint, field, obj, _options) - obj[field.from].upcase + def field(ctx) + ctx.object[ctx.field.from].upcase end - def object(_blueprint, field, obj, _options) - val = obj[field.from] + def object(ctx) + val = ctx.object[ctx.field.from] val.transform_values { |v| v.upcase } end - def collection(_blueprint, field, obj, _options) - vals = obj[field.from] + def collection(ctx) + vals = ctx.object[ctx.field.from] vals.map { |val| val.transform_values { |v| v * 2 } } end end diff --git a/spec/v2/extractor_spec.rb b/spec/v2/extractor_spec.rb index 75364719..d792f3c6 100644 --- a/spec/v2/extractor_spec.rb +++ b/spec/v2/extractor_spec.rb @@ -2,6 +2,7 @@ describe Blueprinter::V2::Extractor do subject { described_class.new } + let(:context) { Blueprinter::V2::Context } let(:blueprint) do Class.new(Blueprinter::V2::Base) do @@ -14,22 +15,23 @@ def upcase(str) context 'field' do it "should extract using a block" do field = Blueprinter::V2::Field.new(from: :foo, value_proc: ->(obj, _opts) { upcase obj[:foo] }) - obj = { foo: 'bar' } - val = subject.field(blueprint.new, field, obj, {}) + ctx = context.new(blueprint.new, field, nil, { foo: 'bar' }, {}) + val = subject.field(ctx) expect(val).to eq 'BAR' end it "should extract using a Hash key" do field = Blueprinter::V2::Field.new(from: :foo) - obj = { foo: 'bar' } - val = subject.field(blueprint.new, field, obj, {}) + ctx = context.new(blueprint.new, field, nil, { foo: 'bar' }, {}) + val = subject.field(ctx) expect(val).to eq 'bar' end it "should extract using a method name" do field = Blueprinter::V2::Field.new(from: :name) obj = Struct.new(:name).new("Foo") - val = subject.field(blueprint.new, field, obj, {}) + ctx = context.new(blueprint.new, field, nil, obj, {}) + val = subject.field(ctx) expect(val).to eq 'Foo' end end @@ -37,22 +39,23 @@ def upcase(str) context 'object' do it "should extract using a block" do field = Blueprinter::V2::ObjectField.new(from: :foo, value_proc: ->(obj, _opts) { upcase obj[:foo] }) - obj = { foo: 'bar' } - val = subject.object(blueprint.new, field, obj, {}) + ctx = context.new(blueprint.new, field, nil, { foo: 'bar' }, {}) + val = subject.object(ctx) expect(val).to eq 'BAR' end it "should extract using a Hash key" do field = Blueprinter::V2::Field.new(from: :foo) - obj = { foo: 'bar' } - val = subject.object(blueprint.new, field, obj, {}) + ctx = context.new(blueprint.new, field, nil, { foo: 'bar' }, {}) + val = subject.object(ctx) expect(val).to eq 'bar' end it "should extract using a method name" do field = Blueprinter::V2::Field.new(from: :name) obj = Struct.new(:name).new("Foo") - val = subject.object(blueprint.new, field, obj, {}) + ctx = context.new(blueprint.new, field, nil, obj, {}) + val = subject.object(ctx) expect(val).to eq 'Foo' end end @@ -60,22 +63,23 @@ def upcase(str) context 'collection' do it "should extract using a block" do field = Blueprinter::V2::Collection.new(from: :foo, value_proc: ->(obj, _opts) { upcase obj[:foo] }) - obj = { foo: 'bar' } - val = subject.collection(blueprint.new, field, obj, {}) + ctx = context.new(blueprint.new, field, nil, { foo: 'bar' }, {}) + val = subject.collection(ctx) expect(val).to eq 'BAR' end it "should extract using a Hash key" do field = Blueprinter::V2::Field.new(from: :foo) - obj = { foo: 'bar' } - val = subject.collection(blueprint.new, field, obj, {}) + ctx = context.new(blueprint.new, field, nil, { foo: 'bar' }, {}) + val = subject.collection(ctx) expect(val).to eq 'bar' end it "should extract using a method name" do field = Blueprinter::V2::Field.new(from: :name) obj = Struct.new(:name).new("Foo") - val = subject.collection(blueprint.new, field, obj, {}) + ctx = context.new(blueprint.new, field, nil, obj, {}) + val = subject.collection(ctx) expect(val).to eq 'Foo' end end