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

Add dynamic field support #164

Merged
merged 7 commits into from Jul 24, 2019
Merged

Conversation

amalarayfreshworks
Copy link
Contributor

@amalarayfreshworks amalarayfreshworks commented Jul 4, 2019

We have a use case in our product where the key names of the result hash are defined at run time, so in order to avoid introducing an unnecessary extra level/key, I have made some changes.

I have introduced one custom field option called "merge". If this option is present we can add the value returned by the "method/name" directly to the obj hash through a merge.

Eg :
let(:blueprint) do
Class.new(Blueprinter::Base) do
identifier :id
field :dynamic_fields, merge: true
end
end
def dynamic_fields
{"field1" => "val1"}
end
blueprint.render(obj) would return '{"id":' + obj_id + ', "field1": "val1"}'
instead of
'{"id":' + obj_id + ', "dynamic_fields": {"field1": "val1"}}'

@@ -3,7 +3,7 @@
FactoryBot.define do
factory :user do
first_name { 'Meg' }
last_name { 'Jones' }
last_name { 'Ryan' }
Copy link
Contributor

Choose a reason for hiding this comment

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

💯

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.

I'm not sure if I like this merge option. I don't think it explains itself very well what merge is doing without context.

I think the second proposal of what @ritikesh mentioned in the gitter here https://gitter.im/blueprinter-gem/community?at=5d1c81b2f0f22f66450825c4 would fall more in line with current blueprinter's APIs.

Considering that we already have existing features like this:

field :something, if: -> { ... }

so doing something like

field :something, name: -> { ... }

would be pretty self explanatory because it sticks with current patterns.

@amalarayfreshworks
Copy link
Contributor Author

amalarayfreshworks commented Jul 9, 2019

@philipqnguyen But aren't we giving additional responsibility to the attribute name, by giving a proc which will return name and value. If at all in future, if we have to support a feature where you can dynamically define the name/key of the hash, won't this approach be ideal for that scenario, where you return the dynamic name through the proc?

@philipqnguyen
Copy link
Contributor

philipqnguyen commented Jul 9, 2019

@amalarayfreshworks I'm not sure if I fully understand.

In this case:

field :something, name: -> { |obj, _opt| obj.dynamic_field_name }

The field :something would be responsible for accessing the value, where as name: -> { |obj, _opt| obj.dynamic_field_name } would evaluate to a new name. I don't see the name proc ever evaluating for both name and value.

Or am I missing something or not understanding you?

@ritikesh
Copy link
Collaborator

ritikesh commented Jul 9, 2019

@philipqnguyen can you pls check Gitter? She has elaborated the use-case there.

@amalarayfreshworks
Copy link
Contributor Author

I have elaborated the usecase in gitter https://gitter.im/blueprinter-gem/community?at=5d24b57317cc7b05ca9c8249. Please do let me know how can we implement the use case by being in line with current blueprinter's APIs.

@mcclayton
Copy link
Contributor

mcclayton commented Jul 10, 2019

The team has been deliberating on how we’d like to address something like this.
We’ve come up with a proposal that we think is generic enough to handle your use case, as well as some others that we’ve been thinking through (such as #158). Would the following suffice your use case?

We are thinking of adding a new Transformer class
i.e.

module Blueprinter
  class Transformer
    attr_reader :primary_obj, :hash, :options

    def initialize(hash, primary_obj, options={})
      @primary_obj = primary_obj
      @hash = hash
      @options = options
    end

    def transform
      fail NotImplementedError, "A Transformer must implement #transform"
    end
  end
end

Users can then implement transformer classes and declare them in a Blueprint as follows:

class UserBlueprint < Blueprinter::Base
  fields :first_name, :last_name

  transform DynamicFieldTransformer
end

class DynamicFieldTransformer < Blueprinter::Transformer
  def transform
    hash.merge!(primary_obj.dynamic_fields)
  end
end

Whatever is returned by the set transform will be the resulting hash passed to serialization (to_json).

@amalarayfreshworks
Copy link
Contributor Author

amalarayfreshworks commented Jul 15, 2019

Can anyone please re trigger the build for ruby 2_5 and ruby 2_4 as the build failed at installing ruby dependencies.
image
@philipqnguyen

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 for doing this!
Some comments!

lib/blueprinter/transformer.rb Outdated Show resolved Hide resolved
@@ -38,6 +43,10 @@ def exclude_fields(field_names)
end
end

def *(custom_transformer)
transformers << custom_transformer
end
Copy link
Contributor

Choose a reason for hiding this comment

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

I've never seen * used for anything but multiplication. Is this common style?

Otherwise, can we come up with a method name that makes clear that it is appending transformer here?
I'm worried we will run out of symbols to define new methods 🤣

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am usually not into operator overloading, but then I was inspired by the method written for adding fields into the view
def <<(field)
fields[field.name] = field
end
I was under the impression that its a style blueprinter follows for adding setters. I agree to all the reviewers @philipqnguyen and @mcclayton when you feel that we can run out of operators to overload. For now I will change the method name to transform :)

Copy link
Contributor

Choose a reason for hiding this comment

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

can you rename to add_transformer? Otherwise it seems like view.transform is already invoking the transformer, which is not entirely accurate since it's just storing it to be called later.

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 @amalarayfreshworks a couple more comments, but overall looking great.

@@ -22,6 +23,10 @@ def inherit(view)
view.excluded_field_names.each do |field_name|
exclude_field(field_name)
end

view.transformers.each do |transformer|
self * transformer
Copy link
Contributor

Choose a reason for hiding this comment

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

you'll want to change this * to the new method name.

@@ -38,6 +43,10 @@ def exclude_fields(field_names)
end
end

def *(custom_transformer)
transformers << custom_transformer
end
Copy link
Contributor

Choose a reason for hiding this comment

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

can you rename to add_transformer? Otherwise it seems like view.transform is already invoking the transformer, which is not entirely accurate since it's just storing it to be called later.

end
end
it('returns json with values derived from options') { should eq(result) }
end
Copy link
Contributor

Choose a reason for hiding this comment

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

Would you also add a test to ensure that the transformers are also inherited? There's some inheritance specs here that you can add to: https://github.com/procore/blueprinter/blame/4ba12dd14edf340f9a4a95f1668785adc8c21ca6/spec/integrations/base_spec.rb#L326-L348

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Test case has been added.

1. Changed setter of transformer from “transform” to “add_transformer”
2. Invoked add_transformer instead of * in view inherit.
3. Added test cases to verify transformer inheritance
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.

LGTM

Copy link
Contributor

@mcclayton mcclayton left a comment

Choose a reason for hiding this comment

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

🚀

@mcclayton mcclayton merged commit 6cb2784 into procore-oss:master Jul 24, 2019
# # other code
# end
#
# class DynamicFieldTransformer < Blueprinter::Transformer
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be DynamicTransformer? (or transform DynamicFieldTransformer above)?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yup, example mismatch. would appreciate if you can make a PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

Done, see #174

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.

None yet

6 participants