Skip to content
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

explicitly track field definition order when used to sort output fields #197

Merged
merged 5 commits into from
Mar 13, 2020

Conversation

wlkrw
Copy link
Contributor

@wlkrw wlkrw commented Jan 28, 2020

This is a fix for #172. Thanks to #185 I saw that the problem comes from how fields are merged at render time. Hash insertion order was not quite enough but probably could have been made to work with some more bookkeeping during the field definition which would then be taken account of in ViewCollection #sortable_fieldsand #merge_fields

Rather than increasing the amount of fancy footwork involved with merging hashes, each View now builds up an ordering at definition time which is traversed at render time.

One edge case to note, is that if for some reason a View is defined over multiple block yields, the output order will be as if all those fields were added inside the first one. This seems no worse than the current situation where all those fields would be grouped together in an incorrect location.

@gresbtim
Copy link

sweet

Copy link
Contributor

@philipqnguyen philipqnguyen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for your work on this issue! I have some comments.


def initialize(name, fields: {}, included_view_names: [], excluded_view_names: [],transformers: [])
@name = name
@fields = fields
@included_view_names = included_view_names
@excluded_field_names = excluded_view_names
@transformers = transformers
@definition_order = []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so @definition_order is an Array of either strings and/or hashes, where strings represents field_names and hashes represents view_name right? That doesn't seem to be very intuitive, do you think there's another way to map the ordering that would be more explicit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Structs with a name and a type (:field or :view) each? The main thing is to avoid the possibility of name collisions between views and fields without hacky magic substrings in the array like "actually_its_a_view_#{view_name}" . include_view support means we can't just stick the view object itself in there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would work!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the last push it is an array of DefinitionPlaceholder instances.

included_fields.merge(source_fields)
def sort_by_def(view_name, fields)
result = views[:default].definition_order.reduce({}) do |memo, key|
if key.is_a?(Hash)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's very implicit right here that a Hash key is actually a view.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So with Structs it would have a case statement on the type here and likewise in the renamed reduce_helper.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good to me.

def sort_by_def(view_name, fields)
result = views[:default].definition_order.reduce({}) do |memo, key|
if key.is_a?(Hash)
reduce_helper(memo, key, fields) if view_name == key.keys.first # recur all the way down on the given view_name but no others!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename reduce_helper to something else that is a little more descriptive?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add_to_ordered_fields is the first thing to come to mind, with ordered_fields naming the first argument.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm OK with that.

Copy link
Contributor

@philipqnguyen philipqnguyen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super minor comment, then I think we're good to go after that.

@@ -1,5 +1,6 @@
module Blueprinter
# @api private
DefinitionPlaceholder = Struct.new :name, :for_a_view?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of for_a_view?, view? would be more ruby like

@wlkrw
Copy link
Contributor Author

wlkrw commented Feb 21, 2020

@philipqnguyen I've had a thought lately - how worried are you about cycles in the notional view inclusion graph? I'm not sure if that's been supported by the previous implementation or not. I've taken no measures to prevent or fail gracefully and it seems like it could lead to infinite recursion now. It'll be quick to add a test to see what happens but I haven't had thoughts about a solution yet.

@wlkrw
Copy link
Contributor Author

wlkrw commented Feb 24, 2020

On a closer look, the infinite recursion happens in sortable_fields, before it even gets to the new #sort_by_def and that's where any cycle detection should happen. So I think that's a new issue waiting to be created by someone with a good reason to do this weird thing, rather than a hold up for merging this.

@philipqnguyen
Copy link
Contributor

@wlkrw I'll give this another review in a few days! Sorry just been a little busy!

Copy link
Contributor

@philipqnguyen philipqnguyen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wlkrw The infinite recursion from cyclical views was already happening previously on the master branch. It's not introduced here. I also don't see any reason to save people from getting themselves into this cyclical hole. This looks good to me.

end
end

def merge_fields(source_fields, included_fields)
source_fields.merge! included_fields
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason we wanted to make this merge! vs merge? If we stick with merge! we should probably change this method name to merge_fields! and there is no longer a need to do: fields = merge_fields!(fields... but I think I'd prefer this method without side-effects.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is notable. I think we can refactor it after, for now I think we can go ahead and merge this

@mcclayton
Copy link
Contributor

One minor comment otherwise it looks good to me!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants