Endpoint
Last updated 04 February 2017 trailblazer-endpoint v2.0Endpoint
defines possible outcomes when running an operation and provides a neat matcher mechanism using the dry-matcher
gem to handle those predefined scenarios.
It is usable without Trailblazer and helps to implements endpoint in all frameworks, including Rails and Hanami.
To get a quick overview how endpoints work in Rails, jump to the →Rails section.
Overview
It replaces the following common pattern with a clean, generic mechanic.
# run operation
result = Song::Create.(...)
# react on outcome:
if result.success? && ....
# do this
elsif result.success? && something_else
# do that
elsif # and so on
In place of hard-to-read and hard-wired decider trees, a simple pattern matching happens behind the scenes.
Trailblazer::Endpoint.new.(result) do |m|
m.success { |result| puts "Model #{result["model"]} was created successfully." }
m.unauthenticated { |result| puts "You ain't root!" }
end
It is very beautiful.
Interface
An endpoint is supposed to be run either in a controller action, or directly hooked to a Rack route. It runs the specified operation, and then inspects the result object to find out what scenario is met.
The only interface is the result object returned by the operation. Endpoint doesn’t know anything about internals of the operation.
result = Song::Create.(...)
Trailblazer::Endpoint.new.(result)
Instead, pre-defined outcomes are matched and trigger behavior in form of handlers.
Outcomes
Possible pre-defined outcomes are:
-
not_found
when a model viaModel
configured as:find_by
is not found. -
unauthenticated
when a policy viaPolicy
reports a breach. -
unauthorized
when a policy viaPolicy
reports a breach (NOT YET IMPLEMENTED). -
created
when an operation successfully ran through the pipetree to create one or more models. -
success
when an operation was run successfully. -
present
when an operation is supposed to load model that will then be presented.
All outcomes are detected via a Matcher
object implemented in the endpoint
gem using pattern matching to do so. Please note that in the current state, those heuristics are still work-in-progress and we need your help to define them properly.
Naturally, you may add your own domain-specific outcomes.
Handlers
While Matcher
is the authoritative source for deciding the state of the operation, it is up to you how to react to those well-defined states. This happens using handlers that you can define manually, or use a built-in set. Currently, we have handlers for Rails controllers and Hanami::Router.
You can pass a block to Endpoint#call
with your own handlers and hand in the Result
object.
# run operation.
result = Song::Create.({ title: "SVT" }, "current_user" => User.root)
Trailblazer::Endpoint.new.(result) do |m|
m.success { |result| puts "Model #{result["model"]} was created successfully." }
m.unauthenticated { |result| puts "You ain't root!" }
end
While the state decisions are abstracted away, handling those outcomes lies in the programmer’s hands.
Handler Proc
You can also organize common outcomes in a callable object, such as a proc.
MyHandlers = ->(m) do
m.success { |result| puts "Model #{result["model"]} was created successfully." }
m.unauthenticated { |result| puts "You ain't root!" }
end
And then, hand them into Endpoint#call
as the second argument.
Trailblazer::Endpoint.new.(result, MyHandlers)
Handler: Proc and Block
When handing in a proc and using a block, the block takes precedence over the proc object’s handlers. This is meant to ad-hoc-override generic behavior.
Trailblazer::Endpoint.new.(result, MyHandlers) do |m|
m.unauthenticated { |result| raise "Break-in!" }
end
With a successful outcome, the generic handler from MyHandlers
is applied. Running this without a current user will raise an exception from the block, though.
Adding Outcomes
Rails
Standard handlers are provided for Rails and are meant to replace responders.
require "trailblazer/endpoint/rails" # do that in `config/initializers/trailblazer.rb`!
class SongsController < ApplicationController
include Trailblazer::Endpoint::Controller
def create
endpoint Song::Create, path: songs_path, args: [ params, { "current_user" => current_user } ]
end
end
endpoint
will run Song::Create
, use the pre-defined matchers to find out the scenario, and run a generic handler for it.
Rails Handlers
The generic behavior works as follows.
-
not_found
callshead 404
. TODO: add descriptions once it’s stable.
Check out the implementation for more details, it’s very readable.
Rails Ad-Hoc Overriding
To run your custom logic in a specific controller action, pass a block to endpoint
.
class SongsController < ApplicationController
def create
endpoint Create, path: songs_path, args: [ params, { "current_user" => current_user } ] do |m|
m.created { |result| render json: result["representer.serializer.class"].new(result["model"]), status: 999 }
end
end
TODO: explain :path
etc, and make it optional.