Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dealing with HTTP errors #13

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ gem "multi_json"

gem "minitest-line"

gem "trailblazer", path: "../trailblazer"
gem "trailblazer"
# gem "trailblazer-operation", path: "../operation"

gem "dry-validation"
gem "dry-validation", "~> 0.11"
94 changes: 48 additions & 46 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
PATH
remote: ../trailblazer
specs:
trailblazer (2.1.0.rc1)
trailblazer-macro (>= 2.1.0.rc1, < 2.2.0)
trailblazer-macro-contract (= 2.1.0.rc1)
trailblazer-operation

PATH
remote: .
specs:
Expand All @@ -15,84 +7,94 @@ PATH
GEM
remote: https://rubygems.org/
specs:
concurrent-ruby (1.0.2)
concurrent-ruby (1.1.5)
declarative (0.0.10)
declarative-builder (0.1.0)
declarative-option (< 0.2.0)
declarative-option (0.1.0)
disposable (0.4.4)
disposable (0.4.7)
declarative (>= 0.0.9, < 1.0.0)
declarative-builder (< 0.2.0)
declarative-option (< 0.2.0)
representable (>= 2.4.0, <= 3.1.0)
uber (< 0.2.0)
dry-configurable (0.3.0)
dry-configurable (0.9.0)
concurrent-ruby (~> 1.0)
dry-container (0.5.0)
dry-core (~> 0.4, >= 0.4.7)
dry-container (0.7.2)
concurrent-ruby (~> 1.0)
dry-configurable (~> 0.1, >= 0.1.3)
dry-core (0.2.0)
dry-core (0.4.9)
concurrent-ruby (~> 1.0)
dry-equalizer (0.3.0)
dry-inflector (0.2.0)
dry-logic (0.6.1)
concurrent-ruby (~> 1.0)
dry-equalizer (0.2.0)
dry-logic (0.4.0)
dry-container (~> 0.2, >= 0.2.6)
dry-core (~> 0.1)
dry-core (~> 0.2)
dry-equalizer (~> 0.2)
dry-matcher (0.5.0)
dry-types (0.9.0)
dry-matcher (0.8.3)
dry-core (>= 0.4.8)
dry-types (0.14.1)
concurrent-ruby (~> 1.0)
dry-configurable (~> 0.1)
dry-container (~> 0.3)
dry-core (~> 0.1)
dry-core (~> 0.4, >= 0.4.4)
dry-equalizer (~> 0.2)
dry-logic (~> 0.4, >= 0.4.0)
inflecto (~> 0.0.0, >= 0.0.2)
dry-validation (0.10.3)
dry-inflector (~> 0.1, >= 0.1.2)
dry-logic (~> 0.5, >= 0.5)
dry-validation (0.13.3)
concurrent-ruby (~> 1.0)
dry-configurable (~> 0.1, >= 0.1.3)
dry-container (~> 0.2, >= 0.2.8)
dry-core (~> 0.1)
dry-core (~> 0.2, >= 0.2.1)
dry-equalizer (~> 0.2)
dry-logic (~> 0.4, >= 0.4.0)
dry-types (~> 0.9, >= 0.9.0)
hirb (0.7.3)
inflecto (0.0.2)
minitest (5.9.1)
minitest-line (0.6.3)
dry-logic (~> 0.5, >= 0.5.0)
dry-types (~> 0.14.0)
minitest (5.14.0)
minitest-line (0.6.5)
minitest (~> 5.0)
multi_json (1.12.1)
rake (11.3.0)
multi_json (1.14.1)
rake (13.0.1)
reform (2.2.4)
disposable (>= 0.4.1)
representable (>= 2.4.0, < 3.1.0)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
trailblazer-activity (0.7.1)
hirb
trailblazer-context
trailblazer-context (0.1.2)
trailblazer-macro (2.1.0.rc1)
trailblazer-macro-contract (2.1.0.rc1)
trailblazer (2.1.0)
trailblazer-macro (>= 2.1.0, < 2.2.0)
trailblazer-macro-contract (>= 2.1.0, < 2.2.0)
trailblazer-operation
trailblazer-activity (0.10.0)
trailblazer-context (>= 0.2.0, < 0.3.0)
trailblazer-activity-dsl-linear (0.2.6)
trailblazer-activity (>= 0.9.1, < 1.0.0)
trailblazer-context (0.2.0)
trailblazer-developer (0.0.10)
representable
trailblazer-activity (>= 0.10.0, < 1.0.0)
trailblazer-activity-dsl-linear
trailblazer-macro (2.1.1)
trailblazer-operation (>= 0.6.0)
trailblazer-macro-contract (2.1.0)
reform (>= 2.2.0, < 3.0.0)
trailblazer-operation (0.4.1)
trailblazer-activity (>= 0.7.1, < 0.8.0)
trailblazer-context (>= 0.1.1, < 0.3.0)
trailblazer-operation (0.6.0)
trailblazer-activity (>= 0.10.0, < 1.0.0)
trailblazer-activity-dsl-linear (>= 0.2.1, < 1.0.0)
trailblazer-developer (>= 0.0.8)
uber (0.1.0)

PLATFORMS
ruby

DEPENDENCIES
bundler
dry-validation
dry-validation (~> 0.11)
minitest
minitest-line
multi_json
rake
trailblazer!
trailblazer
trailblazer-endpoint!

BUNDLED WITH
1.16.2
1.17.2
43 changes: 18 additions & 25 deletions lib/trailblazer/endpoint.rb
Original file line number Diff line number Diff line change
@@ -1,35 +1,28 @@
require "dry/matcher"
require "trailblazer/endpoint/rails"

module Trailblazer
class Endpoint
# this is totally WIP as we need to find best practices.
# also, i want this to be easily extendable.
Matcher = Dry::Matcher.new(
present: Dry::Matcher::Case.new( # DISCUSS: the "present" flag needs some discussion.
match: ->(result) { result.success? && result["present"] },
resolve: ->(result) { result }),
success: Dry::Matcher::Case.new(
match: ->(result) { result.success? },
resolve: ->(result) { result }),
created: Dry::Matcher::Case.new(
match: ->(result) { result.success? && result["model.action"] == :new }, # the "model.action" doesn't mean you need Model.
resolve: ->(result) { result }),
not_found: Dry::Matcher::Case.new(
match: ->(result) { result.failure? && result["result.model"] && result["result.model"].failure? },
resolve: ->(result) { result }),
# TODO: we could add unauthorized here.
unauthenticated: Dry::Matcher::Case.new(
match: ->(result) { result.failure? && result["result.policy.default"].failure? }, # FIXME: we might need a &. here ;)
resolve: ->(result) { result }),
invalid: Dry::Matcher::Case.new(
match: ->(result) { result.failure? && result["result.contract.default"] && result["result.contract.default"].failure? },
resolve: ->(result) { result })
not_found: Dry::Matcher::Case.new { |result| result_error_matching?(result, :not_found) ? result : Dry::Matcher::Undefined },
unauthenticated: Dry::Matcher::Case.new { |result| result_error_matching?(result, :unauthenticated) ? result : Dry::Matcher::Undefined },
unauthorized: Dry::Matcher::Case.new { |result| result_error_matching?(result, :unauthorized) ? result : Dry::Matcher::Undefined },
invalid_params: Dry::Matcher::Case.new { |result| result_error_matching?(result, :invalid_params) ? result : Dry::Matcher::Undefined },
)

# `call`s the operation.
def self.call(operation_class, handlers, *args, &block)
result = operation_class.(*args)
new.(result, handlers, &block)
class << self
# `call`s the operation.
def call(operation_class, handlers, *args, &block)
result = operation_class.(*args)
new.(result, handlers, &block)
end

private

def result_error_matching?(result, state)
end_state = result.event.to_h[:semantic]
result.failure? && end_state == state
end
end

def call(result, handlers=nil, &block)
Expand Down
35 changes: 27 additions & 8 deletions lib/trailblazer/endpoint/rails.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
require "trailblazer/endpoint"

module Trailblazer::Endpoint::Handlers
# Generic matcher handlers for a Rails API backend.
#
Expand All @@ -15,13 +13,34 @@ def initialize(controller, options)

def call
->(m) do
m.not_found { |result| controller.head 404 }
m.unauthenticated { |result| controller.head 401 }
m.present { |result| controller.render json: result["representer.serializer.class"].new(result['model']), status: 200 }
m.created { |result| controller.head 201, location: "#{@path}/#{result["model"].id}" }#, result["representer.serializer.class"].new(result["model"]).to_json
m.success { |result| controller.head 200, location: "#{@path}/#{result["model"].id}" }
m.invalid { |result| controller.render json: result["representer.errors.class"].new(result['result.contract.default'].errors).to_json, status: 422 }
m.not_found { |res| render_json_error(res, 404) }
m.unauthenticated { |res| render_json_error(res, 401) }
m.unauthorized { |res| render_json_error(res, 403) }
m.invalid_params { |res| render_validation_errors(res, 422) }
end
end

private

def render_json_error(ctx, status)
err_msg = ctx['trailblazer-endpoint.error'] || default_for(status)
controller.render(json: err_msg, status: status)
end

def render_validation_errors(ctx, status)
err_msg = ctx['result.contract.default']&.errors&.messages || default_for(status)
controller.render(json: err_msg, status: status)
end

def default_for(status_code)
default_msg = {
401 => 'Unauthorized.',
403 => 'Forbidden.',
404 => 'Resource not found.',
422 => 'Unprocessable entity.'
}

default_msg[status_code]
end
end
end
65 changes: 65 additions & 0 deletions test/endpoint_matcher_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
require 'test_helper'

class EndpointMatcherTest < Minitest::Spec
class TestOperation < Trailblazer::Operation
NotFound = Class.new(Trailblazer::Activity::Signal)
Unauthenticated = Class.new(Trailblazer::Activity::Signal)
Unauthorized = Class.new(Trailblazer::Activity::Signal)
InvalidParams = Class.new(Trailblazer::Activity::Signal)

SignalMatchers = {
not_found: NotFound,
unauthenticated: Unauthenticated,
unauthorized: Unauthorized,
invalid_params: InvalidParams,
}

step ->(_, tested_state:, **) { SignalMatchers[tested_state] || true },
Output(NotFound, :not_found) => End(:not_found),
Output(Unauthenticated, :unauthenticated) => End(:unauthenticated),
Output(Unauthorized, :unauthorized) => End(:unauthorized),
Output(InvalidParams, :invalid_params) => End(:invalid_params)
end

let(:my_handlers) do
->(m) do
m.not_found { |_| @its_a_match = :not_found }
m.unauthenticated { |_| @its_a_match = :unauthenticated }
m.unauthorized { |_| @its_a_match = :unauthorized }
m.invalid_params { |_| @its_a_match = :invalid_params }
end
end

before do
@its_a_match = :no_match
end

# 404 :not_found
it 'matches the :not_found state' do
result = TestOperation.call(tested_state: :not_found)
Trailblazer::Endpoint.new.call(result, my_handlers)

_(@its_a_match).must_equal(:not_found)
end

it 'matches the :unauthenticated state' do
result = TestOperation.call(tested_state: :unauthenticated)
Trailblazer::Endpoint.new.call(result, my_handlers)

_(@its_a_match).must_equal(:unauthenticated)
end

it 'matches the :unauthorized state' do
result = TestOperation.call(tested_state: :unauthorized)
Trailblazer::Endpoint.new.call(result, my_handlers)

_(@its_a_match).must_equal(:unauthorized)
end

it 'matches the :unauthorized state' do
result = TestOperation.call(tested_state: :invalid_params)
Trailblazer::Endpoint.new.call(result, my_handlers)

_(@its_a_match).must_equal(:invalid_params)
end
end
Loading