-
Notifications
You must be signed in to change notification settings - Fork 110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature proposal: Honor all conditional checks config - path for sparse fields #308
Feature proposal: Honor all conditional checks config - path for sparse fields #308
Conversation
a87270c
to
0fd0158
Compare
0fd0158
to
9eb8c9e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good from my perspective. I agree we should be able to combine the conditionals, and roll_up_conditions
makes sense.
README.md
Outdated
|
||
### 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lessthanjacob suggested roll_up_conditions
is a bit unclear. How about enforce_all_conditions
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the ping! You caught me in the middle of writing a quick comment 😆
The desired behavior makes sense to me, but the name of the configuration setting gives me slight pause. Ideally it's something that you can set and forget, but I'd challenge us to think of a name that describes the functionality a bit more explicitly!
I could be on board with enforce_all_conditions
. We could also codify the default behavior in the configuration, and expect that we set it to false
to get the behavior detailed in this PR (e.g. field_level_condition_overrides_global
).
Looking back at this again, I think it could be worth implementing explicit Sparse Field functionality here as a follow up! |
- 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]>
Co-authored-by: Nate Baer <[email protected]> Signed-off-by: Alejandro Reyes <[email protected]>
Co-authored-by: Nate Baer <[email protected]> Signed-off-by: Alejandro Reyes <[email protected]>
Signed-off-by: alejandroereyes <[email protected]>
1d0caac
to
f96ac5e
Compare
@lessthanjacob , @njbbaer Goood call on the name change to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alejandroereyes -- thanks for adding this feature!
Throwing in some ideas here.
- I could go with
enforce_all_conditions
; or I might suggest something using the wordmerge
; e.g.merge_field_options_with_global_options
- Ideally there would be more complete test coverage of the different branches:
- field unless == true but global if == true: merge_options? ? passing : skipping
- field unless == false, but global unless == true: merge_options? ? skipping : passing
- field if == true, but global unless == false: merge_options? ? passing : passing
- invalid
if
orunless
values for either global or field options should raise specific errors
- I do like the break up of methods into more Clean Code; but I might DRY things up some more. Suggestions in-line, below
end | ||
|
||
private | ||
|
||
def any_global_or_field_condition_failed?(field_name, object, local_options) | ||
return true if if_callables.any? { |if_call| !if_call.call(field_name, object, local_options) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not very DRY. It is repeating the same logic as the original skip?
method, but against a set of up to two items, rather than just a single item. I think it would be cleaner if you use the new config setting to help compose the set of conditions to be tested, and only have one method doing the actual testing.
Suggestions below. Feel free to use them, modify them, DRY them up further, or do something else.
else | ||
raise ArgumentError, "#{tmp.class} is passed to :#{condition}" | ||
raise ArgumentError, "#{tmp_callable.class} is passed to :#{condition}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you missed adding a test for this error case, because condition
is not defined for this method--that is the argument passed to the original callable_from
method.
This extraction method is nice, though. It would be nice to keep it.
A slightly clunky solution that would provide a valid error message is to pass in a 2nd argument: condition
, where condition ∈ { 'field option', 'Blueprint configuration' }
as provided by one of the private calling methods, above.
A better solution, I think, would be to
- add explicit
if
andunless
setter methods onBlueprinter::Configuration
and raise appropriate argument errors there if not a Proc or a Symbol - add validation of the
options
param in theField
initializer to ensure that any:if
or:unless
attributes are also either aProc
orSymbol
That way you get errors sooner and with a more meaningful stack trace.
def unless_callables | ||
[ | ||
extract_callable_from(Blueprinter.configuration.unless), | ||
extract_callable_from(options[:unless]) | ||
].select { |callable| callable } | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The following modification should allow this method to be used for both the legacy conditions, and the case where enforce_all_conditions
(or whatever it is called) is set to true
def unless_callables | |
[ | |
extract_callable_from(Blueprinter.configuration.unless), | |
extract_callable_from(options[:unless]) | |
].select { |callable| callable } | |
end | |
def unless_callables | |
callables = [extract_callable_from(options[:unless])] | |
if options[:unless].nil? || enforce_all_conditions | |
callables << extract_callable_from(Blueprinter.configuration.unless), | |
end | |
callables.compact | |
end |
- assumes
extract_callable_from
returnsnil
instead offalse
when passed anil
argument - and something like:
delegate :enforce_all_conditions, to: Blueprinter.configuration
def if_callables | ||
[ | ||
extract_callable_from(Blueprinter.configuration.if), | ||
extract_callable_from(options[:if]) | ||
].select { |callable| callable } | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The following modification should allow this method to be used for both the legacy conditions, and the case where enforce_all_conditions
(or whatever it is called) is set to true
def if_callables | |
[ | |
extract_callable_from(Blueprinter.configuration.if), | |
extract_callable_from(options[:if]) | |
].select { |callable| callable } | |
end | |
def if_callables | |
callables = [extract_callable_from(options[:if])] | |
if options[:if].nil? || enforce_all_conditions | |
callables << extract_callable_from(Blueprinter.configuration.if), | |
end | |
callables.compact | |
end |
- assumes
extract_callable_from
returnsnil
instead offalse
when passed anil
argument - and something like:
delegate :enforce_all_conditions, to: Blueprinter.configuration
@@ -47,13 +71,17 @@ def callable_from(condition) | |||
config.public_send(condition) | |||
end | |||
|
|||
return false unless tmp | |||
extract_callable_from(tmp) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This whole method can go away if all condition gathering is moved to unless_callables
and if_callables
Or, you could refactor it to really DRY things up by pulling in the suggested logic from those two methods, and make this one the the do-everything method. That does have the potential to add too much complexity here, though, which is why I leaned in on the explicitly named methods that you created.
if Blueprinter.configuration.enforce_all_conditions | ||
any_global_or_field_condition_failed?(field_name, object, local_options) | ||
else | ||
return true if if_callable && !if_callable.call(field_name, object, local_options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"true if this and not this" just spins my head a little. The following suggestion is to replace the entire skip?
method, though:
Basically, I might make skip?
work a little more like Rails blank?
and present?
for readability (but in this case, skip?
is !include?
)
if Blueprinter.configuration.enforce_all_conditions | |
any_global_or_field_condition_failed?(field_name, object, local_options) | |
else | |
return true if if_callable && !if_callable.call(field_name, object, local_options) | |
def skip?(field_name, object, local_options) | |
!inclusion_conditions_met?(field_name, object, local_options) | |
end | |
def inclusion_conditions_met?(field, obj, locals) | |
if_callables.any? { |callable| callable.call(field, obj, locals) } && | |
unless_callables.none? { |callable| callable.call(field, obj, locals) } | |
end |
let(:local_options) { {x: 1, y: 2, v1: value, v2: other_value} } | ||
|
||
before do | ||
Blueprinter.configuration.enforce_all_conditions = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting and resetting global configurations for testing is not an ideal practice.
Can this be done instead with
Blueprinter.configuration.enforce_all_conditions = true | |
allow(Blueprinter).to receive(:configuration.enforce_all_conditions).and_return(true) |
and then, as a bonus, you don't need the after
block
|
||
before do | ||
Blueprinter.configuration.enforce_all_conditions = true | ||
Blueprinter.configuration.if = ->(field_name, object, local_opts) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally this could also be stubbed with an allow
condition
value | ||
end | ||
} | ||
Blueprinter.configuration.unless = ->(_a,_b,_c) { other_value } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
Background
At my company we make liberal use of views. But there are times that I find them constricting when trying to optimize payload sizes. I would like to have more granular control over what is returned. I propose adding a configuration to honor all conditional checks. This would allow for a lightweight implementation for sparse fields support over our API.
Example:
Checklist: