Skip to content

Commit

Permalink
add_conditional_rollup
Browse files Browse the repository at this point in the history
- add roll_up_conditions configuration
- when enabled serialization honors all conditionals when
  deciding to render a field
- this allows for a global conditional to be set, opening the
  possibility for sparse_fields to be implemented

Signed-off-by: alejandroereyes <[email protected]>
  • Loading branch information
alejandroereyes committed Aug 22, 2023
1 parent cb8fd7e commit 9eb8c9e
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 10 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -688,13 +688,15 @@ Output:
---

Both the `field` and the global Blueprinter Configuration supports `:if` and `:unless` options that can be used to serialize fields conditionally.
By default field-level conditions override global conditions. By enabling `roll_up_conditions`, BOTH field-level and global conditions must be met in order for field to be serialized.

### Global Config Setting - if and unless

```ruby
Blueprinter.configure do |config|
config.if = ->(field_name, obj, _options) { !obj[field_name].nil? }
config.unless = ->(field_name, obj, _options) { obj[field_name].nil? }
config.roll_up_conditions = true
end
```

Expand All @@ -708,8 +710,6 @@ class UserBlueprint < Blueprinter::Base
end
```

_NOTE:_ The field-level setting overrides the global config setting (for the field) if both are set.

---
</details>

Expand Down
3 changes: 2 additions & 1 deletion lib/blueprinter/configuration.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Blueprinter
class Configuration
attr_accessor :association_default, :datetime_format, :deprecations, :field_default, :generator, :if, :method, :sort_fields_by, :unless, :extractor_default, :default_transformers
attr_accessor :association_default, :datetime_format, :deprecations, :field_default, :generator, :if, :method, :sort_fields_by, :unless, :extractor_default, :default_transformers, :roll_up_conditions

VALID_CALLABLES = %i(if unless).freeze

Expand All @@ -16,6 +16,7 @@ def initialize
@unless = nil
@extractor_default = AutoExtractor
@default_transformers = []
@roll_up_conditions = false
end

def jsonify(blob)
Expand Down
37 changes: 30 additions & 7 deletions lib/blueprinter/field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@ def extract(object, local_options)
end

def skip?(field_name, object, local_options)
return true if if_callable && !if_callable.call(field_name, object, local_options)
unless_callable && unless_callable.call(field_name, object, local_options)
if Blueprinter.configuration.roll_up_conditions
return true if if_callables.any? { |if_call| !if_call.call(field_name, object, local_options) }
unless_callables.any? { |unless_call| unless_call.call(field_name, object, local_options) }
else
return true if if_callable && !if_callable.call(field_name, object, local_options)
unless_callable && unless_callable.call(field_name, object, local_options)
end
end

private
Expand All @@ -25,11 +30,26 @@ def if_callable
@if_callable = callable_from(:if)
end

def if_callables
[
extract_callable_from(Blueprinter.configuration.if),
extract_callable_from(options[:if])

].select { |callable| callable }
end

def unless_callable
return @unless_callable if defined?(@unless_callable)
@unless_callable = callable_from(:unless)
end

def unless_callables
[
extract_callable_from(Blueprinter.configuration.unless),
extract_callable_from(options[:unless])
].select { |callable| callable }
end

def callable_from(condition)
callable = old_callable_from(condition)

Expand All @@ -50,14 +70,17 @@ def old_callable_from(condition)
elsif config.valid_callable?(condition)
config.public_send(condition)
end
extract_callable_from(tmp)
end

return false unless tmp
def extract_callable_from(tmp_callable)
return false unless tmp_callable

case tmp
when Proc then tmp
when Symbol then blueprint.method(tmp)
case tmp_callable
when Proc then tmp_callable
when Symbol then blueprint.method(tmp_callable)
else
raise ArgumentError, "#{tmp.class} is passed to :#{condition}"
raise ArgumentError, "#{tmp_callable.class} is passed to :#{condition}"
end
end
end
37 changes: 37 additions & 0 deletions spec/integrations/shared/base_render_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,43 @@ def self.unless_method(_field_name, _object, _options)
include_examples 'does not serialize the conditional field'
end
end

context 'roll_up_conditions is enabled' do
let(:local_options) { {x: 1, y: 2, v1: value, v2: other_value} }

before do
Blueprinter.configuration.roll_up_conditions = true
Blueprinter.configuration.if = ->(field_name, object, local_opts) {
if local_opts[:sparse_fields]
local_opts[:sparse_fields].include?(field_name.to_s)
else
value
end
}
Blueprinter.configuration.unless = ->(_a,_b,_c) { other_value }
end
after do
Blueprinter.configuration.roll_up_conditions = false
Blueprinter.configuration.if = nil
Blueprinter.configuration.unless = nil
end

if value && !other_value
include_examples 'serializes the conditional field'

context 'and top level conditional triggered by local options' do
let(:local_options) { {sparse_fields: ['first_name']} }

it 'honors all configuration conditionals - only serializes specified field' do
should eq(%({"first_name":"Meg"}))
end
end
else
it 'nothing is serialized' do
should eq('{}')
end
end
end
end
end

Expand Down

0 comments on commit 9eb8c9e

Please sign in to comment.