blueprinter icon indicating copy to clipboard operation
blueprinter copied to clipboard

Private method helpers?

Open Frexuz opened this issue 3 years ago • 6 comments

class Api::V1::CountrySerializer < ActiveModel::Serializer
  attributes :name

  attribute :currency_name do
    currency&.name
  end

  attribute :currency_code do
    currency&.iso_code
  end

  private
    def currency
      @currency ||= ISO3166::Country[object.alpha2]&.currency
    end
end

AMS has 'global' access to object inside the class.

Does blueprinter have something similar? I haven't found anything in the readme.

I've only managed to do it without lookup

  field :currency_name do |country|
    ISO3166::Country[country.alpha2]&.currency.name
  end

  field :currency_code do |country|
    ISO3166::Country[country.alpha2]&.currency.iso_code
  end

But that could be expensive in some cases.

I could also do

  field :currency do |country|
    c = ISO3166::Country[country.alpha2]
    {
       name: c&.name,
       code: c&.iso_code
    } 
  end

but i'd prefer not.., and in some cases that might not be feasible

Frexuz avatar Jul 01 '21 12:07 Frexuz

I think to accomplish something similar in Blueprinter, you'd want something like:

class Api::V1::CountryBlueprint < Blueprinter::Base
  field :currency_code do |object|
    currency_for(object)&.iso_code
  end

  private
  def self.currency_for(obj)
    @@currency[obj.name] ||= ISO3166::Country[obj]&.currency
  end
end

mcclayton avatar Jul 19 '21 22:07 mcclayton

Will try later this week! Will let you know :)

kg-currenxie avatar Jul 20 '21 01:07 kg-currenxie

Hm, getting an error

"exception": "#<NameError: uninitialized class variable @@currency in Api::V1::CountrySerializer\nDid you mean?  currency_for>",

code

  field :currency_code do |country|
    currency_for(country)&.iso_code
  end

  field :currency_name do |country|
    currency_for(country)&.name
  end

  private
    def self.currency_for(obj)
      @@currency[obj.alpha2] ||= ISO3166::Country[obj.alpha2]&.currency
    end

Not sure how to initialize it before usage? 🤔

I also tried @currency ||= , but that caches it once "forever" and every subsequent call won't give me the correct value.

im fine with this for now:

    def self.currency_for(obj)
      ISO3166::Country[obj.alpha2]&.currency
    end

the lookup is fast, and at least i solved the duplication. But would be nice to know how to fix it :D

Frexuz avatar Jul 22 '21 11:07 Frexuz

This should initialize correct class variable:

@@currency = {}

However class variable will be bloated over time so unless you are sure it will be garbage collected it's not safe to do this.

Since Blueprinter doesn't instantiate Blueprint class while constructing json and uses blocks instead you are probably supposed to build currency object outside of Blueprint and then pass it as an option:

CountryBlueprint < Blueprinter::Base
  field :currency_code do |_country, options|
    options[:currency]&.iso_code
  end

  field :currency_name do |_country, options|
    options[:currency]&.name
  end
end

CountryBlueprint.render(country, currency: ISO3166::Country[country.alpha2])

But you also can use this options hash to share the context between fields, despite it's a hack you may still want to use it:

class CountryBlueprint < Blueprinter::Base
  field :currency_name do |country, options|
    currency_for(country, options)&.currency.name
  end

  field :currency_code do |country, options|
    currency_for(country, options)&.currency.iso_code
  end

  private

  def self.currency_for(obj, opts)
    opts[:currency] ||= ISO3166::Country[obj.alpha2]
  end
end

sl4vr avatar Aug 31 '21 20:08 sl4vr

Agree with the desire for these kinds of helpers. Would it be possible to consider a more structural change to the API of blueprinter, to avoid the heavy use of class methods? That would make helpers more intuitive.

For instance: the recommendations above that call for defining helpers as class methods all make the mistake of presuming that private affects class methods. It does not.

@mcclayton / @sl4vr

private
def self.currency_for(obj);end

This still results in CountryBlueprint.currency_for being a public method. I think this points to one of the huge drawbacks of using class methods and (IMO) it would be an improvement to restructure the api to use regular old instances. (The api could be made backwards compatible by making the class methods instantiate under the covers)

I'll admit that I hoped/assumed the blueprint api was more similar to AMS and graphql-ruby's apis; which both support attribute overrides via method definition (which I think is an improvement over blocks) and more streamlined delegation support.

jasonkarns avatar Sep 21 '21 15:09 jasonkarns

Hm, building it outside the helper like CountryBlueprint.render(country, currency: ISO3166::Country[country.alpha2]) doesn't work for collections :)

kg-currenxie avatar Dec 01 '21 11:12 kg-currenxie

This issue 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.

github-actions[bot] avatar Nov 02 '23 01:11 github-actions[bot]