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_foundwhen a model viaModelconfigured as:find_byis not found. -
unauthenticatedwhen a policy viaPolicyreports a breach. -
unauthorizedwhen a policy viaPolicyreports a breach (NOT YET IMPLEMENTED). -
createdwhen an operation successfully ran through the pipetree to create one or more models. -
successwhen an operation was run successfully. -
presentwhen 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_foundcallshead 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.
