Skip to content

Instantly share code, notes, and snippets.

@mrbongiolo
Last active October 12, 2015 22:11
Show Gist options
  • Save mrbongiolo/681e8ee828da7f79dff1 to your computer and use it in GitHub Desktop.
Save mrbongiolo/681e8ee828da7f79dff1 to your computer and use it in GitHub Desktop.
Trailblazer Operation usecase using a Decorator instead of the Representer.

Decorator

https://gist.github.com/mrbongiolo/bbabbda0bbd8f57769a0

With provider: :local

I receive a simple customer hash like that:

{ provider: 'local', name: 'Wilhelm Reich', email: '[email protected]' }

And I need to return this:

{
  "customer": {
    "id": "SUCH_HASH_MUCH_IDNESS",
    "token": "MY_API_TOKEN",
    "name": "Wilhelm Reich",
    "email": "[email protected]"
  }
}

With provider: :remote

I receive a simple customer hash like that:

{ provider: 'remote', code: 'the_remote_api_auth_code' }

And I need to return this:

{
  "customer": {
    "id": "ANOTHER_HASHED_ID",
    "token": "AND_ANOTHER_TOKEN",
    "name": "Sigmund Freud",
    "email": "[email protected]"
  }
}

Other ways

This could be accomplished by calling a custom Decorator on the valid block when running the operation.

Something like that:

run Api::Customer::Create do |op|
  return render json: Api::CustomerDecorator.new( op.model ).to_json
end

By doing this I could remove the Decorator and Representer from the operation, actually I (think) could still leave Representer in to enable parsing from different sources.

I tried this schema using Representer instead of Decorator, but was getting some weird results and errors, mostly because if I set self.representation_wrap = :customer the parsing expects to receive the data as: customer: {}. And when I sent in data like that the parser returned some errors, I guess it happens because Representer has to do this params[:comment] = request.body as shown here: http://trailblazerb.org/gems/operation/controller.html#respond.

My main "concern" with Representer is that it expects the same document to be used for parsing and rendering, that's ok for most cases, but it seems to become a mess when that's not the case.

I had a case where I could redeem a voucher for a resource, something like:

# POST /vouchers/redeem { voucher: 'SUCH_CODE_MUCH_DISCOUNT', resource_id: 'CRAZY_ID_HERE' }

# The Voucher::Redeem operation would check it and if everthing was OK apply the voucher to the resource

# The valid response was the updated :resource

As shown above, I can "easily" set the response by calling the desired Decorator on the controller, but I was thinking if it wouldn't be better to add this kind of behaviour to the Operation, so that we can call operation.to_json, operation.to_xml or operation.to_whatever_crazy_format_i_need.

It's a way to set on the Operation what it expects to receive and what it will output.

I'm just trying to understand and find the best way to handle those situations, maybe an Operation shouldn't care about the "result". It will just receive some data, do stuff on it, and I can decide what to do with it.

class Api::Customer::Create < Trailblazer::Operation
include Model
include Decorator
model Customer, :create
builds -> (params) do
case params[:provider].to_s
when 'local'
return Local
when 'remote'
return Remote
end
end
contract do
property :provider, virtual: true
validates: {
presence: true,
inclusion: { in: %w(local remote) }
}
end
decorator do
self.representation_wrap = :customer
property :hashed_id, as: :id
property :api_token, as: :token, default: ''
property :name
property :email
end
def process(params)
validate(params) do
contract.save
end
end
private
# I want to decorated the model, not the contract right now.
def decorated
model
end
end
class Api::Customer::Create::Local < Api::Customer::Create
contract do
property :name
property :email
validates :name, presence: true
validates :email, presence: true, email: true
end
end
class Api::Customer::Create::Remote < Api::Customer::Create
contract do
property :code, virtual: true
end
def process(params)
# get data from the Remote API and finish the Customer creation
end
end
@apotonick
Copy link

Cool, thanks for this! I agree that if you have different ins and outs, you don't wanna use the same representer, absolutely not!

@timoschilling and I had this idea:

run Comment::Create, presenter: Comment::Create::Presenter, deserializer: SomethingCompletelyDifferent

That way, it's up to the user to configure it should it be different behaviour than the default (use same for both).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment