Skip to content

Commit

Permalink
Pass a Context to V2 extractor
Browse files Browse the repository at this point in the history
Signed-off-by: Jordan Hollinger <[email protected]>
  • Loading branch information
jhollinger committed Nov 12, 2024
1 parent 480d093 commit 339775a
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 34 deletions.
6 changes: 3 additions & 3 deletions lib/blueprinter/v2/extensions/values.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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))
Expand All @@ -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))
Expand Down
23 changes: 13 additions & 10 deletions lib/blueprinter/v2/extractor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions lib/blueprinter/v2/fields.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 6 additions & 6 deletions spec/v2/extensions/extraction_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 19 additions & 15 deletions spec/v2/extractor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,68 +15,71 @@ 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

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

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
Expand Down

0 comments on commit 339775a

Please sign in to comment.