diff --git a/lib/blueprinter/extension.rb b/lib/blueprinter/extension.rb index f6c75dca..65010af6 100644 --- a/lib/blueprinter/extension.rb +++ b/lib/blueprinter/extension.rb @@ -5,10 +5,10 @@ module Blueprinter # Base class for all extensions. # # V2 hook call order: - # - sort_fields # - collection? (skipped if calling render_object/render_collection) # - input_object | input_collection # - prepare + # - blueprint_fields # - blueprint_input # - exclude_field? # - field_value @@ -23,16 +23,6 @@ module Blueprinter # - pre_render # class Extension - # - # Returns fields in the order they should appear. Default is the order in which they were defined. - # - # @param fields [Array] - # @return [Array] - # - def sort_fields(fields) - fields - end - # # Returns true if the given object should be treated as a collection (i.e. supports `map { |obj| ... }`). # @@ -51,6 +41,18 @@ def collection?(_object) # def prepare(context); end + # + # Returns the fields that should be included in the correct order. Default is all fields in the order in which they were defined. + # + # NOTE Only runs once per Blueprint per render. + # + # @param context [Blueprinter::V2::Context] + # @return [Array] + # + def blueprint_fields(ctx) + [] + end + # # Modify or replace the object passed to render/render_object. # diff --git a/lib/blueprinter/v2/extensions/collections.rb b/lib/blueprinter/v2/extensions/collections.rb index b01e79f3..52e30846 100644 --- a/lib/blueprinter/v2/extensions/collections.rb +++ b/lib/blueprinter/v2/extensions/collections.rb @@ -12,6 +12,10 @@ def collection?(object) else false end end + + def blueprint_fields(ctx) + ctx.blueprint.class.reflections[:default].ordered + end end end end diff --git a/lib/blueprinter/v2/extensions/field_order.rb b/lib/blueprinter/v2/extensions/field_order.rb index 987ac2fe..b5cb94c7 100644 --- a/lib/blueprinter/v2/extensions/field_order.rb +++ b/lib/blueprinter/v2/extensions/field_order.rb @@ -8,8 +8,8 @@ def initialize(&sorter) @sorter = sorter end - def sort_fields(fields) - fields.sort(&@sorter) + def blueprint_fields(ctx) + ctx.blueprint.class.reflections[:default].ordered.sort(&@sorter) end end end diff --git a/lib/blueprinter/v2/reflection.rb b/lib/blueprinter/v2/reflection.rb index ca90aff5..92d9d8e4 100644 --- a/lib/blueprinter/v2/reflection.rb +++ b/lib/blueprinter/v2/reflection.rb @@ -39,8 +39,8 @@ class View attr_reader :objects # @return [Hash] Associations to collections defined on the view attr_reader :collections - # @return [Array] All fields, objects, and collections ordered by the field_order extension hook (default is the order in which they were defined) - attr_reader :sorted + # @return [Array] All fields, objects, and collections in the order they were defined + attr_reader :ordered # @param blueprint [Class] A subclass of Blueprinter::V2::Base @@ -48,7 +48,7 @@ class View # @api private def initialize(blueprint, name) @name = name - @sorted = blueprint.serializer.hooks.last(:sort_fields, blueprint.schema.values) || blueprint.schema.values + @ordered = blueprint.schema.values @fields = blueprint.schema.select { |_, f| f.is_a? Field } @objects = blueprint.schema.select { |_, f| f.is_a? ObjectField } @collections = blueprint.schema.select { |_, f| f.is_a? Collection } diff --git a/lib/blueprinter/v2/serializer.rb b/lib/blueprinter/v2/serializer.rb index cf0bd63c..cf54b9c1 100644 --- a/lib/blueprinter/v2/serializer.rb +++ b/lib/blueprinter/v2/serializer.rb @@ -19,11 +19,11 @@ def initialize(blueprint) end def call(object, options, instances, data) + data[blueprint.object_id] ||= prepare! Context.new(instances[blueprint], nil, nil, nil, options, instances, data) ctx = Context.new(instances[blueprint], nil, nil, object, options, instances, data) - ctx.data[blueprint.object_id] ||= prepare! ctx hooks.reduce_into(:blueprint_input, ctx, :object) if @run_blueprint_input - result = blueprint.reflections[:default].sorted.each_with_object({}) do |field, acc| + result = ctx.data[blueprint.object_id][:fields].each_with_object({}) do |field, acc| ctx.field = field ctx.value = nil @@ -66,7 +66,7 @@ def prepare!(ctx) values.prepare ctx exclusions.prepare ctx hooks.each(:prepare, ctx) if @run_prepare - true + { fields: hooks.last(:blueprint_fields, ctx).freeze }.freeze end def block_unused_hooks! diff --git a/spec/extensions/extension_spec.rb b/spec/extensions/extension_spec.rb index 056eda28..18c2099e 100644 --- a/spec/extensions/extension_spec.rb +++ b/spec/extensions/extension_spec.rb @@ -9,9 +9,9 @@ let(:object) { { foo: 'Foo' } } let(:context) { Blueprinter::V2::Context } - it 'should default sort fields to the given order' do - fields = [Blueprinter::V2::Field.new(name: :foo), Blueprinter::V2::Field.new(name: :bar)] - expect(subject.new.sort_fields(fields)).to eq fields + it 'should default to no fields' do + ctx = context.new(blueprint.new, nil, nil, object, {}) + expect(subject.new.blueprint_fields(ctx)).to eq [] end it 'should default collections? to false' do diff --git a/spec/v2/extensions/collections_spec.rb b/spec/v2/extensions/collections_spec.rb index 88ec6f6a..78fbcc9f 100644 --- a/spec/v2/extensions/collections_spec.rb +++ b/spec/v2/extensions/collections_spec.rb @@ -41,4 +41,15 @@ x = OpenStruct.new expect(subject.collection? x).to be false end + + it 'should return all fields in the order they were defined' do + blueprint = Class.new(Blueprinter::V2::Base) do + field :name + object :category, self + collection :parts, self + end + ctx = Blueprinter::V2::Context.new(blueprint.new, nil, nil, nil, {}, {}, {}) + + expect(subject.blueprint_fields(ctx).map(&:name)).to eq %i(name category parts) + end end diff --git a/spec/v2/extensions/field_order_spec.rb b/spec/v2/extensions/field_order_spec.rb index 8858e882..0727a316 100644 --- a/spec/v2/extensions/field_order_spec.rb +++ b/spec/v2/extensions/field_order_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true describe Blueprinter::V2::Extensions::FieldOrder do + let(:context) { Blueprinter::V2::Context.new(blueprint.new, nil, nil, nil, {}, {}, {}) } let(:blueprint) do Class.new(Blueprinter::V2::Base) do field :foo @@ -11,7 +12,7 @@ it 'should sort fields alphabetically' do ext = described_class.new { |a, b| a.name <=> b.name } - result = ext.sort_fields(blueprint.schema.values) + result = ext.blueprint_fields(context) expect(result.map(&:name)).to eq %i(bar foo id) end @@ -25,7 +26,7 @@ a.name <=> b.name end end - result = ext.sort_fields(blueprint.schema.values) + result = ext.blueprint_fields(context) expect(result.map(&:name)).to eq %i(id bar foo) end end diff --git a/spec/v2/reflection_spec.rb b/spec/v2/reflection_spec.rb index b83b9502..8644a237 100644 --- a/spec/v2/reflection_spec.rb +++ b/spec/v2/reflection_spec.rb @@ -90,33 +90,12 @@ expect(blueprint.reflections[:extended].collections.keys).to eq %i(widgets) end - it 'should be in the default order' do - names = blueprint.reflections[:default].sorted.map(&:name) + it 'should be in the definition order' do + names = blueprint.reflections[:default].ordered.map(&:name) expect(names).to eq %i(name category) - names = blueprint.reflections[:extended].sorted.map(&:name) + names = blueprint.reflections[:extended].ordered.map(&:name) expect(names).to eq %i(name category widgets description) end - - it 'should be in a custom order' do - ext = Class.new(Blueprinter::Extension) do - def initialize(&sorter) - @sorter = sorter - end - - def sort_fields(fields) - fields.sort(&@sorter) - end - end - - blueprint.extensions << ext.new { |a, b| b.name <=> a.name } - blueprint.extensions << ext.new { |a, b| a.name <=> b.name } - - names = blueprint.reflections[:default].sorted.map(&:name) - expect(names).to eq %i(category name) - - names = blueprint.reflections[:extended].sorted.map(&:name) - expect(names).to eq %i(category description name widgets) - end end end diff --git a/spec/v2/serializer_spec.rb b/spec/v2/serializer_spec.rb index 9652ade0..074d8c26 100644 --- a/spec/v2/serializer_spec.rb +++ b/spec/v2/serializer_spec.rb @@ -56,6 +56,27 @@ }) end + it 'should respect the blueprint_fields hook' do + ext = Class.new(Blueprinter::Extension) do + def blueprint_fields(ctx) + ctx.blueprint.class.reflections[:default].ordered.sort_by(&:name) + end + end + widget_blueprint.extensions << ext.new + widget = { + name: 'Foo', + category: { name: 'Bar' }, + parts: [{ num: 42 }, { num: 43 }] + } + + result = described_class.new(widget_blueprint).call(widget, {}, instance_cache, {}) + expect(result.to_json).to eq({ + category: { name: 'Bar' }, + name: 'Foo', + parts: [{ num: 42 }, { num: 43 }] + }.to_json) + end + it 'should enable the if conditionals extension' do widget_blueprint = Class.new(Blueprinter::V2::Base) do field :name