trailblazer / representable

Maps representation documents from and to Ruby objects. Includes JSON, XML and YAML support, plain properties and compositions.
http://trailblazer.to/2.1/docs/representable.html
MIT License
689 stars 108 forks source link

Passing options from a Representer and non-wrapping property #144

Open mrbongiolo opened 9 years ago

mrbongiolo commented 9 years ago

1 - Passing Options when re-using Representers

Maybe this is possible, but I couldn't find it out. I'm re-using a Representer within another Representer, and I would like to pass "user options" to it, just like I can do when calling MyRepresenter.new(model).to_json(foo: :bar).

Something like this:

class AddressRepresenter < Representable::Decorator
  property :district
  property :street, if: ->(opts) { opts[:confirmed] }
end

class CustomerRepresenter < Representable::Decorator
  property :name

  property :address, decorator: AddressRepresenter, confirmed: confirmed?
end

I tried it this way, but the :confirmed option does not show up in the opts.

Right now I solved my issue by creating 2 different Representers and caling the correct one within a lambda, this way: decorator: ->(*) { confirmed? ? ConfirmedAddressRepresenter : UnconfirmedAddressRepresenter }. This seems ok, but for some cases it would be simpler to just pass options from one Representer to another.

2 - Non-wrapping property

Another case that I faced when re-using a Representer is this one:

class AddressRepresenter < Representable::Decorator
  property :district
  property :street, if: ->(opts) { opts[:confirmed] }
end

class CustomerAddressRepresenter < Representable::Decorator
  property :address, decorator: AddressRepresenter
  property :confirmed
end

When I render a JSON collection from the CustomerAddressRepresenter I get this:

[
  {
     "address": {
       "district": "Arouba",
       "street": "2nd"
     },
     "confirmed": true
  }
]

and what I really need is this:

[
  {
     "district": "Arouba",
     "street": "2nd",
     "confirmed": true
  }
]

In this case the CustomerAddress is a model used to link the Customer to an Address, and store some extra fields, like the :confirmed. But for the API response this should be transparent, a CustomerAddress should be just like an Address, but with the extra fields.

That's how I "solved" this:

class CustomerAddressRepresenter < Representable::Decorator
  property :district,
    getter: ->(*) { address.district }
  property :street, 
    getter: ->(*) { address.street },
    if: ->(opts) { opts[:confirmed] }
  property :confirmed
end

It's pretty ugly, but worked for me, maybe my case is very edgy and no one else needed something like this.

I was thinking in two ways to solve this, the first one is to use a non_wrapping: true or something like this, but this could get confused with the wrap: false option used to disable the representation_wrap of a Representer. Ex:

class CustomerAddressRepresenter < Representable::Decorator
  property :address, decorator: AddressRepresenter, non_wrapping: true
  property :confirmed
end

The second way is to add all properties manually in the Representer, but have an option to set the correct represented model. Ex:

class CustomerAddressRepresenter < Representable::Decorator
  property :district,
    represented: -> { address } #or should we call it accessor?
  property :street, 
    represented: -> { address },
    if: ->(opts) { opts[:confirmed] }
  property :confirmed
end

What are your thoughts on those issues? If needed just point me where I could start and I might be able to send a PR.

apotonick commented 9 years ago
  1. Interesting problem! :grimacing:

    What about this?

    class CustomerRepresenter < Representable::Decorator
     property :name
    
     property :address, decorator: AddressRepresenter
    
     def to_hash(*)
       super(confirmed: confirmed?)
     end
    end
  2. The goal of Representable is to map documents to and from Ruby objects. You're correctly using :getter in your case as this is the only way to achieve the format you desire given your object graph.

However, I don't like using this option at all. You should create an object that represents the document structure and then feed this to the representer. Remember: Representable is not a data mapping framework.

The easiest is to create an OpenStruct object that represents the properties as you want them in the document. You might also be interested in Disposable which actually is a data mapper and not a representation gem. You would then feed the Twin to the representer.

I am guessing you're working with Rails which makes you believe that one object should do everything in an acts_as_awesome_fu way. This is wrong.

Let your models be persisting objects. Let the intermediate object graph (OpenStruct, twin) be your data mapping. Let the representer render and parse this graph. Don't try to achieve all of that in one asset. If you don't want your controller to be doing this, consider using operations from Trailblazer. I'm here to assist you, either way! :stuck_out_tongue_winking_eye: :

mrbongiolo commented 9 years ago

Thanks for you answers.

I'll try your first suggestion and see how it goes.

Right now I'm using Representable just to render my json data, I had it all in json views using jbuilder. I know that it can also be used to parse data from json to my objects, but didn't used it yet.

So as you suggest I could create a CustomerAddress < Disposable::Twin right? This object would be responsible for the compositions and mapping them correctly to the models.

class CustomerAddressRepresenter < Representable::Decorator
  property :district
  property :street, if: ->(opts) { opts[:confirmed] }
  property :confirmed
end

class CustomerAddressTwin < Disposable::Twin
  include Composition

  property :district, on: :address
  property :street, on: :address
  property :confirmed, on: :customer_address
end

# and when rendering it should be called like that?

customer_address_twin = CustomerAddressTwin
  .new(customer_address: customer_address, address: customer_address.address)
CustomerAddressRepresenter.new( customer_address_twin ).to_json

Or am I missing something? And BTW I'm already using operations on most of my endpoints, still have a few left to refactor :smile:

apotonick commented 9 years ago

Awesome, good to hear you are using operations! :laughing:

What you pasted is 100% correct. You can actually infer twins from representers and vice-versa, but in your case, this looks fine to me. Let me know how it goes.

BTW, we are planning a Crystal-port of Trb where we can define data types on the language level (fast, in C). And one of the extensions will be twins, so you have twins just like you use integers and strings.