trailblazer-endpoint 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGES.md +3 -0
- data/Gemfile +16 -0
- data/README.md +11 -0
- data/Rakefile +10 -0
- data/lib/trailblazer-endpoint.rb +1 -0
- data/lib/trailblazer/endpoint.rb +133 -0
- data/lib/trailblazer/endpoint/adapter.rb +197 -0
- data/lib/trailblazer/endpoint/builder.rb +56 -0
- data/lib/trailblazer/endpoint/protocol.rb +122 -0
- data/lib/trailblazer/endpoint/rails.rb +27 -0
- data/lib/trailblazer/endpoint/version.rb +5 -0
- data/test/adapter/api_test.rb +78 -0
- data/test/adapter/representable_test.rb +7 -0
- data/test/adapter/web_test.rb +40 -0
- data/test/benchmark/skill_resolver_benchmark.rb +43 -0
- data/test/docs/controller_test.rb +92 -0
- data/test/docs/endpoint_test.rb +54 -0
- data/test/endpoint_test.rb +908 -0
- data/test/rails-app/.gitignore +21 -0
- data/test/rails-app/Gemfile +17 -0
- data/test/rails-app/Gemfile.lock +157 -0
- data/test/rails-app/README.md +24 -0
- data/test/rails-app/Rakefile +6 -0
- data/test/rails-app/app/controllers/application_controller.rb +3 -0
- data/test/rails-app/app/controllers/songs_controller.rb +26 -0
- data/test/rails-app/app/models/application_record.rb +3 -0
- data/test/rails-app/app/models/concerns/.keep +0 -0
- data/test/rails-app/config.ru +5 -0
- data/test/rails-app/config/application.rb +15 -0
- data/test/rails-app/config/boot.rb +3 -0
- data/test/rails-app/config/database.yml +25 -0
- data/test/rails-app/config/environment.rb +5 -0
- data/test/rails-app/config/environments/development.rb +54 -0
- data/test/rails-app/config/environments/production.rb +86 -0
- data/test/rails-app/config/environments/test.rb +42 -0
- data/test/rails-app/config/initializers/application_controller_renderer.rb +6 -0
- data/test/rails-app/config/initializers/backtrace_silencers.rb +7 -0
- data/test/rails-app/config/initializers/cookies_serializer.rb +5 -0
- data/test/rails-app/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/rails-app/config/initializers/inflections.rb +16 -0
- data/test/rails-app/config/initializers/mime_types.rb +4 -0
- data/test/rails-app/config/initializers/new_framework_defaults.rb +24 -0
- data/test/rails-app/config/initializers/session_store.rb +3 -0
- data/test/rails-app/config/initializers/wrap_parameters.rb +14 -0
- data/test/rails-app/config/locales/en.yml +23 -0
- data/test/rails-app/config/routes.rb +6 -0
- data/test/rails-app/config/secrets.yml +22 -0
- data/test/rails-app/db/seeds.rb +7 -0
- data/test/rails-app/log/.keep +0 -0
- data/test/rails-app/test/controllers/.keep +0 -0
- data/test/rails-app/test/controllers/songs_controller_test.rb +156 -0
- data/test/rails-app/test/fixtures/.keep +0 -0
- data/test/rails-app/test/fixtures/files/.keep +0 -0
- data/test/rails-app/test/helpers/.keep +0 -0
- data/test/rails-app/test/integration/.keep +0 -0
- data/test/rails-app/test/mailers/.keep +0 -0
- data/test/rails-app/test/models/.keep +0 -0
- data/test/rails-app/test/test_helper.rb +10 -0
- data/test/rails-app/tmp/.keep +0 -0
- data/test/rails-app/vendor/assets/javascripts/.keep +0 -0
- data/test/rails-app/vendor/assets/stylesheets/.keep +0 -0
- data/test/test_helper.rb +34 -0
- data/trailblazer-endpoint.gemspec +26 -0
- metadata +236 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
require "trailblazer/endpoint"
|
2
|
+
|
3
|
+
module Trailblazer::Endpoint::Handlers
|
4
|
+
# Generic matcher handlers for a Rails API backend.
|
5
|
+
#
|
6
|
+
# Note that the path mechanics are experimental. PLEASE LET US KNOW WHAT
|
7
|
+
# YOU NEED/HOW YOU DID IT: https://gitter.im/trailblazer/chat
|
8
|
+
class Rails
|
9
|
+
def initialize(controller, options)
|
10
|
+
@controller = controller
|
11
|
+
@path = options[:path]
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :controller
|
15
|
+
|
16
|
+
def call
|
17
|
+
->(m) do
|
18
|
+
m.not_found { |result| controller.head 404 }
|
19
|
+
m.unauthenticated { |result| controller.head 401 }
|
20
|
+
m.present { |result| controller.render json: result["representer.serializer.class"].new(result['model']), status: 200 }
|
21
|
+
m.created { |result| controller.head 201, location: "#{@path}/#{result["model"].id}" }#, result["representer.serializer.class"].new(result["model"]).to_json
|
22
|
+
m.success { |result| controller.head 200, location: "#{@path}/#{result["model"].id}" }
|
23
|
+
m.invalid { |result| controller.render json: result["representer.errors.class"].new(result['result.contract.default'].errors).to_json, status: 422 }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class AdapterAPITest < Minitest::Spec
|
4
|
+
it "Adapter::API" do
|
5
|
+
protocol = Class.new(Trailblazer::Endpoint::Protocol) do # DISCUSS: what to to with authenticate and policy?
|
6
|
+
include T.def_steps(:authenticate, :policy)
|
7
|
+
end
|
8
|
+
|
9
|
+
endpoint =
|
10
|
+
Trailblazer::Endpoint.build(
|
11
|
+
domain_activity: activity,
|
12
|
+
protocol: protocol, # do we cover all usual routes?
|
13
|
+
adapter: Trailblazer::Endpoint::Adapter::API,
|
14
|
+
scope_domain_ctx: false,
|
15
|
+
) do
|
16
|
+
|
17
|
+
|
18
|
+
{Output(:not_found) => Track(:not_found)}
|
19
|
+
end
|
20
|
+
|
21
|
+
# success
|
22
|
+
assert_route endpoint, {}, :authenticate, :policy, :model, :validate, :success, status: 200
|
23
|
+
# authentication error
|
24
|
+
assert_route endpoint, {authenticate: false}, :authenticate, :fail_fast, status: 401 # fail_fast == protocol error
|
25
|
+
# policy error
|
26
|
+
assert_route endpoint, {policy: false}, :authenticate, :policy, :fail_fast, status: 403 # fail_fast == protocol error
|
27
|
+
# (domain) not_found err
|
28
|
+
assert_route endpoint, {model: false}, :authenticate, :policy, :model, :fail_fast, status: 404 # fail_fast == protocol error
|
29
|
+
# (domain) validation err
|
30
|
+
assert_route endpoint, {validate: false}, :authenticate, :policy, :model, :validate, :failure, status: 422
|
31
|
+
end
|
32
|
+
|
33
|
+
it "Adapter::API with error_handlers" do
|
34
|
+
protocol = Class.new(Trailblazer::Endpoint::Protocol) do # DISCUSS: what to to with authenticate and policy?
|
35
|
+
include T.def_steps(:authenticate, :policy)
|
36
|
+
end
|
37
|
+
|
38
|
+
adapter = Trailblazer::Endpoint::Adapter::API
|
39
|
+
adapter = Trailblazer::Endpoint::Adapter::API.insert_error_handler_steps(adapter)
|
40
|
+
adapter.include(Trailblazer::Endpoint::Adapter::API::Errors::Handlers)
|
41
|
+
|
42
|
+
# puts Trailblazer::Developer.render(adapter)
|
43
|
+
|
44
|
+
endpoint =
|
45
|
+
Trailblazer::Endpoint.build(
|
46
|
+
domain_activity: activity,
|
47
|
+
protocol: protocol, # do we cover all usual routes?
|
48
|
+
adapter: adapter,
|
49
|
+
scope_domain_ctx: false,
|
50
|
+
|
51
|
+
) do
|
52
|
+
|
53
|
+
|
54
|
+
{Output(:not_found) => Track(:not_found)}
|
55
|
+
end
|
56
|
+
|
57
|
+
class TestErrors < Struct.new(:message)
|
58
|
+
def ==(b)
|
59
|
+
b.message == message
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# success
|
64
|
+
assert_route endpoint, ctx.merge({}), :authenticate, :policy, :model, :validate, :success, status: 200, errors: TestErrors.new(nil)
|
65
|
+
# authentication error
|
66
|
+
assert_route endpoint, ctx.merge({authenticate: false}), :authenticate, :fail_fast, status: 401, errors: TestErrors.new("Authentication credentials were not provided or are invalid.") # fail_fast == protocol error
|
67
|
+
# policy error
|
68
|
+
assert_route endpoint, ctx.merge({policy: false}), :authenticate, :policy, :fail_fast, status: 403, errors: TestErrors.new("Action not allowed due to a policy setting.") # fail_fast == protocol error
|
69
|
+
# (domain) not_found err
|
70
|
+
assert_route endpoint, ctx.merge({model: false}), :authenticate, :policy, :model, :fail_fast, status: 404, errors: TestErrors.new(nil) # fail_fast == protocol error
|
71
|
+
# (domain) validation err
|
72
|
+
assert_route endpoint, ctx.merge({validate: false}), :authenticate, :policy, :model, :validate, :failure, status: 422, errors: TestErrors.new("The submitted data is invalid.")
|
73
|
+
end
|
74
|
+
|
75
|
+
def ctx
|
76
|
+
{errors: TestErrors.new}
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class AdapterWebTest < Minitest::Spec
|
4
|
+
it "AdapterWeb / perfect 2.1 OP scenario (with {not_found} terminus" do
|
5
|
+
activity = Class.new(Trailblazer::Activity::Railway) do
|
6
|
+
step :model, Output(:failure) => End(:not_found)
|
7
|
+
step :validate
|
8
|
+
|
9
|
+
include T.def_steps(:validate, :model)
|
10
|
+
end
|
11
|
+
|
12
|
+
protocol = Class.new(Trailblazer::Endpoint::Protocol) do # DISCUSS: what to to with authenticate and policy?
|
13
|
+
include T.def_steps(:authenticate, :policy)
|
14
|
+
end
|
15
|
+
|
16
|
+
endpoint =
|
17
|
+
Trailblazer::Endpoint.build(
|
18
|
+
domain_activity: activity,
|
19
|
+
protocol: protocol, # do we cover all usual routes?
|
20
|
+
adapter: Trailblazer::Endpoint::Adapter::Web,
|
21
|
+
scope_domain_ctx: false,
|
22
|
+
) do
|
23
|
+
|
24
|
+
|
25
|
+
{Output(:not_found) => Track(:not_found)}
|
26
|
+
end
|
27
|
+
|
28
|
+
# success
|
29
|
+
assert_route(endpoint, {}, :authenticate, :policy, :model, :validate, :success)
|
30
|
+
# authentication error
|
31
|
+
assert_route endpoint, {authenticate: false}, :authenticate, :fail_fast # fail_fast == protocol error
|
32
|
+
# policy error
|
33
|
+
assert_route endpoint, {policy: false}, :authenticate, :policy, :fail_fast # fail_fast == protocol error
|
34
|
+
# (domain) not_found err
|
35
|
+
assert_route endpoint, {model: false}, :authenticate, :policy, :model, :fail_fast # fail_fast == protocol error
|
36
|
+
# (domain) validation err
|
37
|
+
assert_route endpoint, {validate: false}, :authenticate, :policy, :model, :validate, :failure
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "trailblazer/operation"
|
2
|
+
require "benchmark/ips"
|
3
|
+
|
4
|
+
initialize_hash = {}
|
5
|
+
10.times do |i|
|
6
|
+
initialize_hash["bla_#{i}"] = i
|
7
|
+
end
|
8
|
+
|
9
|
+
normal_container = {}
|
10
|
+
50.times do |i|
|
11
|
+
normal_container["xbla_#{i}"] = i
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
Benchmark.ips do |x|
|
16
|
+
x.report(:merge) {
|
17
|
+
attrs = normal_container.merge(initialize_hash)
|
18
|
+
10.times do |i|
|
19
|
+
attrs["bla_8"]
|
20
|
+
end
|
21
|
+
10.times do |i|
|
22
|
+
attrs["xbla_1"]
|
23
|
+
end
|
24
|
+
}
|
25
|
+
|
26
|
+
x.report(:resolver) {
|
27
|
+
attrs = Trailblazer::Skill::Resolver.new(initialize_hash, normal_container)
|
28
|
+
|
29
|
+
10.times do |i|
|
30
|
+
attrs["bla_8"]
|
31
|
+
end
|
32
|
+
10.times do |i|
|
33
|
+
attrs["xbla_1"]
|
34
|
+
end
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
# Warming up --------------------------------------
|
39
|
+
# merge 3.974k i/100ms
|
40
|
+
# resolver 6.593k i/100ms
|
41
|
+
# Calculating -------------------------------------
|
42
|
+
# merge 39.678k (± 9.1%) i/s - 198.700k in 5.056653s
|
43
|
+
# resolver 68.928k (± 6.4%) i/s - 342.836k in 5.001610s
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class DocsControllerTest < Minitest::Spec
|
4
|
+
it "what" do
|
5
|
+
endpoint "view?" do |ctx|
|
6
|
+
# 200, success
|
7
|
+
return
|
8
|
+
end
|
9
|
+
|
10
|
+
# 422
|
11
|
+
# but also 404 etc
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
class Controller
|
16
|
+
def initialize(endpoint, activity)
|
17
|
+
@___activity = activity
|
18
|
+
@endpoint = endpoint
|
19
|
+
@seq = []
|
20
|
+
end
|
21
|
+
|
22
|
+
def view(params)
|
23
|
+
endpoint "view?", params do |ctx|
|
24
|
+
@seq << :success
|
25
|
+
# 200, success
|
26
|
+
return
|
27
|
+
end
|
28
|
+
|
29
|
+
@seq << :failure
|
30
|
+
# 422
|
31
|
+
# but also 404 etc
|
32
|
+
end
|
33
|
+
|
34
|
+
def call(action, **params)
|
35
|
+
send(action, **params)
|
36
|
+
@seq
|
37
|
+
end
|
38
|
+
|
39
|
+
private def endpoint(action, params, &block)
|
40
|
+
ctx = Trailblazer::Endpoint.advance_from_controller(@endpoint,
|
41
|
+
event_name: "",
|
42
|
+
success_block: block,
|
43
|
+
failure_block: ->(*) { return },
|
44
|
+
protocol_failure_block: ->(*) { @seq << 401 and return },
|
45
|
+
|
46
|
+
collaboration: @___activity,
|
47
|
+
domain_ctx: {},
|
48
|
+
success_id: "fixme",
|
49
|
+
flow_options: {},
|
50
|
+
|
51
|
+
**params,
|
52
|
+
|
53
|
+
# DISCUSS: do we really like that fuzzy API? if yes, why do we need {additional_endpoint_options} or whatever it's called?
|
54
|
+
seq: @seq,
|
55
|
+
)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
it "injected {return} interrupts the controller action" do
|
60
|
+
protocol = Class.new(Trailblazer::Endpoint::Protocol)do
|
61
|
+
include T.def_steps(:authenticate, :policy)
|
62
|
+
end
|
63
|
+
|
64
|
+
endpoint =
|
65
|
+
Trailblazer::Endpoint.build(
|
66
|
+
domain_activity: activity,
|
67
|
+
protocol: protocol,
|
68
|
+
adapter: Trailblazer::Endpoint::Adapter::Web,
|
69
|
+
scope_domain_ctx: false,
|
70
|
+
|
71
|
+
) do
|
72
|
+
{Output(:not_found) => Track(:not_found)}
|
73
|
+
end
|
74
|
+
|
75
|
+
# 200
|
76
|
+
seq = Controller.new(endpoint, activity).call(:view)
|
77
|
+
seq.must_equal [:authenticate, :policy, :model, :validate, :success] # the {return} works.
|
78
|
+
|
79
|
+
# 401
|
80
|
+
seq = Controller.new(endpoint, activity).call(:view, authenticate: false)
|
81
|
+
seq.must_equal [:authenticate, :policy, :model, :validate, :success]
|
82
|
+
end
|
83
|
+
|
84
|
+
it "what" do
|
85
|
+
endpoint "view?" do |ctx|
|
86
|
+
# 200, success
|
87
|
+
return
|
88
|
+
end.Or() do |ctx|
|
89
|
+
# Only 422
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class DocsEndpointTest < Minitest::Spec
|
4
|
+
|
5
|
+
# Show how handlers can be put onto a specific path, e.g. {handle_not_authenticated}.
|
6
|
+
module A
|
7
|
+
module Pro
|
8
|
+
module Endpoint
|
9
|
+
class Protocol < Trailblazer::Endpoint::Protocol
|
10
|
+
|
11
|
+
# put {handle_not_authorized} on the respective protocol path.
|
12
|
+
|
13
|
+
# we currently have to squeeze those handlers using the {:before} option, otherwise the path's End is placed before the handler in the sequence. A grouping feature could help.
|
14
|
+
|
15
|
+
step :handle_not_authenticated, magnetic_to: :not_authenticated, Output(:success) => Track(:not_authenticated), Output(:failure) => Track(:not_authenticated)
|
16
|
+
step :handle_not_authorized, magnetic_to: :not_authorized, Output(:success) => Track(:not_authorized), Output(:failure) => Track(:not_authorized)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it "allows to insert a (handle_not_authenticated} path step" do
|
23
|
+
Trailblazer::Developer.render(A::Pro::Endpoint::Protocol).must_equal %{
|
24
|
+
#<Start/:default>
|
25
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=authenticate>
|
26
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=authenticate>
|
27
|
+
{Trailblazer::Activity::Left} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=handle_not_authenticated>
|
28
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=policy>
|
29
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=policy>
|
30
|
+
{Trailblazer::Activity::Left} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=handle_not_authorized>
|
31
|
+
{Trailblazer::Activity::Right} => Trailblazer::Endpoint::Protocol::Noop
|
32
|
+
Trailblazer::Endpoint::Protocol::Noop
|
33
|
+
{#<Trailblazer::Activity::End semantic=:failure>} => #<End/:failure>
|
34
|
+
{#<Trailblazer::Activity::End semantic=:success>} => #<End/:success>
|
35
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=handle_not_authenticated>
|
36
|
+
{Trailblazer::Activity::Left} => #<Trailblazer::Endpoint::Protocol::Failure/:not_authenticated>
|
37
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Endpoint::Protocol::Failure/:not_authenticated>
|
38
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=handle_not_authorized>
|
39
|
+
{Trailblazer::Activity::Left} => #<Trailblazer::Endpoint::Protocol::Failure/:not_authorized>
|
40
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Endpoint::Protocol::Failure/:not_authorized>
|
41
|
+
#<End/:success>
|
42
|
+
|
43
|
+
#<Trailblazer::Endpoint::Protocol::Failure/:invalid_data>
|
44
|
+
|
45
|
+
#<Trailblazer::Endpoint::Protocol::Failure/:not_found>
|
46
|
+
|
47
|
+
#<Trailblazer::Endpoint::Protocol::Failure/:not_authorized>
|
48
|
+
|
49
|
+
#<Trailblazer::Endpoint::Protocol::Failure/:not_authenticated>
|
50
|
+
|
51
|
+
#<End/:failure>
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,908 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
module Trailblazer
|
4
|
+
class Endpoint_ < Trailblazer::Activity::Railway
|
5
|
+
|
6
|
+
|
7
|
+
class PolicyChain < Trailblazer::Activity::Railway
|
8
|
+
step :is_root?, Output(:success) => End(:success) # bypass policy chain
|
9
|
+
# step :a?
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# TODO: document :track_color
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
class Basic_EndpointTest < Minitest::Spec
|
19
|
+
it "{Protocol} is very simple and has no handlers" do
|
20
|
+
protocol = Class.new(Trailblazer::Endpoint::Protocol)
|
21
|
+
|
22
|
+
Trailblazer::Developer.render(protocol).must_equal %{
|
23
|
+
#<Start/:default>
|
24
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=authenticate>
|
25
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=authenticate>
|
26
|
+
{Trailblazer::Activity::Left} => #<Trailblazer::Endpoint::Protocol::Failure/:not_authenticated>
|
27
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=policy>
|
28
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=policy>
|
29
|
+
{Trailblazer::Activity::Left} => #<Trailblazer::Endpoint::Protocol::Failure/:not_authorized>
|
30
|
+
{Trailblazer::Activity::Right} => Trailblazer::Endpoint::Protocol::Noop
|
31
|
+
Trailblazer::Endpoint::Protocol::Noop
|
32
|
+
{#<Trailblazer::Activity::End semantic=:failure>} => #<End/:failure>
|
33
|
+
{#<Trailblazer::Activity::End semantic=:success>} => #<End/:success>
|
34
|
+
#<End/:success>
|
35
|
+
|
36
|
+
#<Trailblazer::Endpoint::Protocol::Failure/:invalid_data>
|
37
|
+
|
38
|
+
#<Trailblazer::Endpoint::Protocol::Failure/:not_found>
|
39
|
+
|
40
|
+
#<Trailblazer::Endpoint::Protocol::Failure/:not_authorized>
|
41
|
+
|
42
|
+
#<Trailblazer::Endpoint::Protocol::Failure/:not_authenticated>
|
43
|
+
|
44
|
+
#<End/:failure>
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
it "{Protocol::Standard} has handlers for 401, 403, 422" do
|
49
|
+
protocol = Class.new(Trailblazer::Endpoint::Protocol::Standard)
|
50
|
+
|
51
|
+
Trailblazer::Developer.render(protocol).must_equal %{
|
52
|
+
#<Start/:default>
|
53
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=authenticate>
|
54
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=authenticate>
|
55
|
+
{Trailblazer::Activity::Left} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=handle_not_authenticated>
|
56
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=policy>
|
57
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=policy>
|
58
|
+
{Trailblazer::Activity::Left} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=handle_not_authorized>
|
59
|
+
{Trailblazer::Activity::Right} => Trailblazer::Endpoint::Protocol::Noop
|
60
|
+
Trailblazer::Endpoint::Protocol::Noop
|
61
|
+
{#<Trailblazer::Activity::End semantic=:failure>} => #<End/:failure>
|
62
|
+
{#<Trailblazer::Activity::End semantic=:success>} => #<End/:success>
|
63
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=handle_not_authenticated>
|
64
|
+
{Trailblazer::Activity::Left} => #<Trailblazer::Endpoint::Protocol::Failure/:not_authenticated>
|
65
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Endpoint::Protocol::Failure/:not_authenticated>
|
66
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=handle_not_authorized>
|
67
|
+
{Trailblazer::Activity::Left} => #<Trailblazer::Endpoint::Protocol::Failure/:not_authorized>
|
68
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Endpoint::Protocol::Failure/:not_authorized>
|
69
|
+
#<End/:success>
|
70
|
+
|
71
|
+
#<Trailblazer::Endpoint::Protocol::Failure/:invalid_data>
|
72
|
+
|
73
|
+
#<Trailblazer::Endpoint::Protocol::Failure/:not_found>
|
74
|
+
|
75
|
+
#<Trailblazer::Endpoint::Protocol::Failure/:not_authorized>
|
76
|
+
|
77
|
+
#<Trailblazer::Endpoint::Protocol::Failure/:not_authenticated>
|
78
|
+
|
79
|
+
#<End/:failure>
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
it "{Adapter::Web} has status setters for 401, 403, 422" do
|
84
|
+
protocol = Class.new(Trailblazer::Endpoint::Adapter::Web)
|
85
|
+
|
86
|
+
Trailblazer::Developer.render(protocol).must_equal %{
|
87
|
+
#<Start/:default>
|
88
|
+
{Trailblazer::Activity::Right} => Trailblazer::Endpoint::Protocol
|
89
|
+
Trailblazer::Endpoint::Protocol
|
90
|
+
{#<Trailblazer::Activity::End semantic=:failure>} => #<End/:failure>
|
91
|
+
{#<Trailblazer::Activity::End semantic=:success>} => #<End/:success>
|
92
|
+
{#<Trailblazer::Endpoint::Protocol::Failure semantic=:not_authorized>} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=_403_status>
|
93
|
+
{#<Trailblazer::Endpoint::Protocol::Failure semantic=:not_found>} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=_404_status>
|
94
|
+
{#<Trailblazer::Endpoint::Protocol::Failure semantic=:not_authenticated>} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=_401_status>
|
95
|
+
{#<Trailblazer::Endpoint::Protocol::Failure semantic=:invalid_data>} => #<End/:failure>
|
96
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=_403_status>
|
97
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=protocol_failure>
|
98
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=_404_status>
|
99
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=protocol_failure>
|
100
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=_401_status>
|
101
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=protocol_failure>
|
102
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=protocol_failure>
|
103
|
+
{Trailblazer::Activity::Left} => #<End/:fail_fast>
|
104
|
+
{Trailblazer::Activity::Right} => #<End/:fail_fast>
|
105
|
+
#<End/:success>
|
106
|
+
|
107
|
+
#<End/:pass_fast>
|
108
|
+
|
109
|
+
#<End/:fail_fast>
|
110
|
+
|
111
|
+
#<End/:failure>
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
it "{Adapter::API} has status setters for 401, 403, 422, error handlers (message)" do
|
116
|
+
protocol = Class.new(Trailblazer::Endpoint::Adapter::API)
|
117
|
+
|
118
|
+
Trailblazer::Developer.render(protocol).must_equal %{
|
119
|
+
#<Start/:default>
|
120
|
+
{Trailblazer::Activity::Right} => Trailblazer::Endpoint::Protocol
|
121
|
+
Trailblazer::Endpoint::Protocol
|
122
|
+
{#<Trailblazer::Activity::End semantic=:failure>} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=failure_render_config>
|
123
|
+
{#<Trailblazer::Activity::End semantic=:success>} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=success_render_config>
|
124
|
+
{#<Trailblazer::Endpoint::Protocol::Failure semantic=:not_authorized>} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=_403_status>
|
125
|
+
{#<Trailblazer::Endpoint::Protocol::Failure semantic=:not_found>} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=_404_status>
|
126
|
+
{#<Trailblazer::Endpoint::Protocol::Failure semantic=:not_authenticated>} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=_401_status>
|
127
|
+
{#<Trailblazer::Endpoint::Protocol::Failure semantic=:invalid_data>} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=failure_render_config>
|
128
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=_403_status>
|
129
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=render_protocol_failure_config>
|
130
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=_404_status>
|
131
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=protocol_failure>
|
132
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=_401_status>
|
133
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=_401_error_message>
|
134
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=_401_error_message>
|
135
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=render_protocol_failure_config>
|
136
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=failure_render_config>
|
137
|
+
{Trailblazer::Activity::Left} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=failure_config_status>
|
138
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=failure_config_status>
|
139
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=failure_config_status>
|
140
|
+
{Trailblazer::Activity::Left} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=render_failure>
|
141
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=render_failure>
|
142
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=render_failure>
|
143
|
+
{Trailblazer::Activity::Left} => #<End/:failure>
|
144
|
+
{Trailblazer::Activity::Right} => #<End/:failure>
|
145
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=success_render_config>
|
146
|
+
{Trailblazer::Activity::Left} => #<End/:failure>
|
147
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=success_render_status>
|
148
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=success_render_status>
|
149
|
+
{Trailblazer::Activity::Left} => #<End/:failure>
|
150
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=render_success>
|
151
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=render_success>
|
152
|
+
{Trailblazer::Activity::Left} => #<End/:failure>
|
153
|
+
{Trailblazer::Activity::Right} => #<End/:success>
|
154
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=render_protocol_failure_config>
|
155
|
+
{Trailblazer::Activity::Left} => #<End/:failure>
|
156
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=render_protocol_failure>
|
157
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=render_protocol_failure>
|
158
|
+
{Trailblazer::Activity::Right} => #<Trailblazer::Activity::TaskBuilder::Task user_proc=protocol_failure>
|
159
|
+
#<Trailblazer::Activity::TaskBuilder::Task user_proc=protocol_failure>
|
160
|
+
{Trailblazer::Activity::Right} => #<End/:fail_fast>
|
161
|
+
#<End/:success>
|
162
|
+
|
163
|
+
#<End/:pass_fast>
|
164
|
+
|
165
|
+
#<End/:fail_fast>
|
166
|
+
|
167
|
+
#<End/:failure>
|
168
|
+
}
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
class EndpointTest < Minitest::Spec
|
174
|
+
# policies
|
175
|
+
# policy.success?
|
176
|
+
# invoke
|
177
|
+
# Workflow::Advance [or simple OP]
|
178
|
+
# success? [standardized "result" object that holds end signal and ctx]
|
179
|
+
|
180
|
+
# controller [this code must be executed in the controller instance, but should be "Rails independent"]
|
181
|
+
# "injectable" policy (maybe also on controller level, configurable)
|
182
|
+
|
183
|
+
# if success yield
|
184
|
+
# Or.()
|
185
|
+
|
186
|
+
|
187
|
+
# test with authenticated user
|
188
|
+
# without user but for a "free" action
|
189
|
+
|
190
|
+
module Model
|
191
|
+
def model(ctx, seq:, model:true, **)
|
192
|
+
ctx[:model] = Struct.new(:name).new("Yo") if model
|
193
|
+
seq << :model
|
194
|
+
model
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
|
199
|
+
# Example OP with three termini
|
200
|
+
class Create < Trailblazer::Activity::Railway
|
201
|
+
include T.def_steps(:model, :validate, :save, :cc_check)
|
202
|
+
|
203
|
+
step :model, Output(:failure) => End(:not_found)
|
204
|
+
step :cc_check, Output(:failure) => End(:cc_invalid)
|
205
|
+
step :validate, Output(:failure) => End(:my_validation_error)
|
206
|
+
step :save
|
207
|
+
|
208
|
+
include Model
|
209
|
+
end
|
210
|
+
|
211
|
+
# Represents a classic FastTrack OP without additional ends.
|
212
|
+
# Implicit termini:
|
213
|
+
# model => not_found
|
214
|
+
# cc_check => cc_invalid
|
215
|
+
# validate => invalid_data
|
216
|
+
class LegacyCreate < Trailblazer::Activity::FastTrack
|
217
|
+
include T.def_steps(:my_policy, :model, :validate, :save, :cc_check)
|
218
|
+
|
219
|
+
step :my_policy
|
220
|
+
step :model
|
221
|
+
step :cc_check, fail_fast: true
|
222
|
+
step :validate
|
223
|
+
step :save
|
224
|
+
include Model
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
# we want to define API and Protocol somewhere application-wide in an explicit file.
|
229
|
+
# the domain OP/wiring we want via the endpoint builder.
|
230
|
+
|
231
|
+
module MyTest
|
232
|
+
# This implements the actual authentication, policies, etc.
|
233
|
+
class Protocol < Trailblazer::Endpoint::Protocol
|
234
|
+
# include EndpointTest::T.def_steps(:authenticate, :handle_not_authenticated, :policy, :handle_not_authorized, :handle_not_found)
|
235
|
+
|
236
|
+
[:authenticate, :handle_not_authenticated, :policy, :handle_not_authorized, :handle_not_found].each do |name|
|
237
|
+
define_method(name) do |ctx, **|
|
238
|
+
ctx[:domain_ctx][:seq] << name
|
239
|
+
return false if ctx[name] === false
|
240
|
+
true
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
|
245
|
+
# TODO: how can we make this better overridable in the endpoint generator?
|
246
|
+
def success?(ctx, domain_activity_return_signal:, domain_ctx:, **)
|
247
|
+
|
248
|
+
# FIXME: stupid test to see if we can read {:domain_activity_return_signal}.
|
249
|
+
ctx[:_domain_activity_return_signal] = domain_activity_return_signal
|
250
|
+
|
251
|
+
|
252
|
+
return Trailblazer::Endpoint::Protocol::Bridge::NotFound if domain_ctx[:model] === false
|
253
|
+
return Trailblazer::Endpoint::Protocol::Bridge::NotAuthorized if domain_ctx[:my_policy] === false
|
254
|
+
# for all other cases, the return value doesn't matter in {fail}.
|
255
|
+
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
|
261
|
+
class MyApiAdapter < Trailblazer::Endpoint::Adapter::API
|
262
|
+
# example how to add your own step to a certain path
|
263
|
+
# FIXME: :after doesn't work
|
264
|
+
step :my_401_handler, before: :_401_status, magnetic_to: :_401, Output(:success) => Track(:_401), Output(:failure) => Track(:_401)
|
265
|
+
|
266
|
+
# def render_success(ctx, **)
|
267
|
+
# ctx[:json] = %{#{ctx[:representer]}.new(#{ctx[:model]})}
|
268
|
+
# end
|
269
|
+
|
270
|
+
def failure_config_status(ctx, **)
|
271
|
+
# DISCUSS: this is a bit like "success?" or a matcher.
|
272
|
+
if ctx[:domain_ctx][:validate] === false
|
273
|
+
ctx[:status] = 422
|
274
|
+
else
|
275
|
+
ctx[:status] = 200 # DISCUSS: this is the usual return code for application/domain errors, I guess?
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# how/where would we configure each endpoint? (per action)
|
280
|
+
# class Endpoint
|
281
|
+
# representer ...
|
282
|
+
# message ...
|
283
|
+
|
284
|
+
def my_401_handler(ctx, domain_ctx:, errors:, **)
|
285
|
+
errors.message = "No token"
|
286
|
+
|
287
|
+
domain_ctx[:seq] << :my_401_handler
|
288
|
+
end
|
289
|
+
end # MyApiAdapter
|
290
|
+
|
291
|
+
api_create_endpoint =
|
292
|
+
Trailblazer::Endpoint.build(
|
293
|
+
adapter: MyApiAdapter,
|
294
|
+
protocol: Class.new(MyTest::Protocol) do # FIXME: why do we have to open a new class to inject the handler method?
|
295
|
+
def handle_invalid_data(ctx, errors:, **) # FIXME: should this be part of the library?
|
296
|
+
errors.message = "The submitted data is invalid."
|
297
|
+
# DISCUSS: here, we could "translate" the {contract.errors}.
|
298
|
+
end
|
299
|
+
|
300
|
+
def handle_not_authorized(ctx, errors:, **)
|
301
|
+
errors.message = "Action not allowed due to a policy."
|
302
|
+
end
|
303
|
+
|
304
|
+
step :handle_not_authorized, magnetic_to: :not_authorized, Output(:success) => Track(:not_authorized), Output(:failure) => Track(:not_authorized), before: "End.not_authorized"
|
305
|
+
end,
|
306
|
+
domain_activity: Create,
|
307
|
+
) do
|
308
|
+
### PROTOCOL ###
|
309
|
+
|
310
|
+
step :handle_invalid_data, magnetic_to: :invalid_data, Output(:success) => Track(:invalid_data)
|
311
|
+
|
312
|
+
# these are arguments for the Protocol.domain_activity
|
313
|
+
{
|
314
|
+
# wire a non-standardized application error to its semantical pendant.
|
315
|
+
Output(:my_validation_error) => Track(:invalid_data), # non-protocol, "application" output
|
316
|
+
# Output(:not_found) => Track(:not_found),
|
317
|
+
|
318
|
+
# wire an unknown end to failure.
|
319
|
+
Output(:cc_invalid) => Track(:failure), # application error.
|
320
|
+
|
321
|
+
Output(:not_found) => _Path(semantic: :not_found) do # _Path will use {End(:not_found)} and thus reuse the terminus already created in Protocol.
|
322
|
+
step :handle_not_found # FIXME: don't require steps in path!
|
323
|
+
end
|
324
|
+
}
|
325
|
+
end
|
326
|
+
|
327
|
+
api_legacy_create_endpoint =
|
328
|
+
Trailblazer::Endpoint.build(
|
329
|
+
# DISCUSS: how do we implement a 201 route?
|
330
|
+
adapter: Class.new(MyApiAdapter) { def success_render_status(ctx, **)
|
331
|
+
ctx[:status] = 201
|
332
|
+
end },
|
333
|
+
protocol: Trailblazer::Endpoint::Protocol::Bridge.insert(MyTest::Protocol).class_eval do
|
334
|
+
def handle_not_authorized(ctx, errors:, **)
|
335
|
+
errors.message = "Action not allowed due to a policy."
|
336
|
+
end
|
337
|
+
|
338
|
+
step :handle_not_authorized, magnetic_to: :not_authorized, Output(:success) => Track(:not_authorized), Output(:failure) => Track(:not_authorized), before: "End.not_authorized"
|
339
|
+
|
340
|
+
self
|
341
|
+
end,
|
342
|
+
domain_activity: LegacyCreate,
|
343
|
+
) do
|
344
|
+
|
345
|
+
|
346
|
+
# Implicit termini:
|
347
|
+
# model => not_found
|
348
|
+
# cc_check => cc_invalid
|
349
|
+
# validate => invalid_data
|
350
|
+
|
351
|
+
### PROTOCOL ###
|
352
|
+
# these are arguments for the Protocol.domain_activity
|
353
|
+
{
|
354
|
+
Output(:fail_fast) => Track(:failure),
|
355
|
+
# TODO: pass_fast test
|
356
|
+
# TODO: do we want to wire those ends to an ongoing "binary" protocol?
|
357
|
+
|
358
|
+
# wire a non-standardized application error to its semantical pendant.
|
359
|
+
# Output(:my_validation_error) => Track(:invalid_data), # non-protocol, "application" output
|
360
|
+
# Output(:not_found) => Track(:not_found),
|
361
|
+
|
362
|
+
# wire an unknown end to failure.
|
363
|
+
# Output(:cc_invalid) => Track(:failure), # application error.
|
364
|
+
|
365
|
+
# Output(:not_found) => _Path(semantic: :not_found) do # _Path will use {End(:not_found)} and thus reuse the terminus already created in Protocol.
|
366
|
+
# step :handle_not_found # FIXME: don't require steps in path!
|
367
|
+
# end
|
368
|
+
}
|
369
|
+
end
|
370
|
+
|
371
|
+
###############3 TODO #######################
|
372
|
+
# test to wire 411 to another track (existing, known, automatically wired)
|
373
|
+
# test wiring an unknown terminus like "cc_not_accepted" to "failure"
|
374
|
+
|
375
|
+
|
376
|
+
# TODO: should we also add a 411 route per default?
|
377
|
+
# how do implement #success? ? in Protocol for sure
|
378
|
+
|
379
|
+
|
380
|
+
# Here we test overriding an entire "endpoint", we want to replace {authenticate} and remove {policy} and the actual {activity}.
|
381
|
+
class Gemauth < api_create_endpoint
|
382
|
+
step Subprocess(
|
383
|
+
MyTest::Protocol,
|
384
|
+
patch: {[] => ->(*) {
|
385
|
+
step nil, delete: :policy
|
386
|
+
step nil, delete: :domain_activity
|
387
|
+
step :gemserver_authenticate, replace: :authenticate, id: :authenticate, inherit: true
|
388
|
+
|
389
|
+
def gemserver_authenticate(ctx, domain_ctx:, gemserver_authenticate:true, **)
|
390
|
+
return false if ctx[:gemserver_authenticate] === false
|
391
|
+
|
392
|
+
domain_ctx[:seq] << :gemserver_authenticate
|
393
|
+
domain_ctx[:model] = Struct.new(:name).new("Gemserver says yes.")
|
394
|
+
end
|
395
|
+
}
|
396
|
+
}), replace: :protocol, inherit: true, id: :protocol
|
397
|
+
end
|
398
|
+
|
399
|
+
|
400
|
+
# step Invoke(), Output(:failure) => Track(:render_fail), Output(:my_validation_error) => ...
|
401
|
+
|
402
|
+
# Invoke(Create, )
|
403
|
+
|
404
|
+
|
405
|
+
# for login form, etc
|
406
|
+
# endpoint, skip: [:authenticate]
|
407
|
+
|
408
|
+
# workflow always terminates on wait events/termini => somewhere, we need to interpret that
|
409
|
+
# OP ends on terminus
|
410
|
+
|
411
|
+
class Errors < Struct.new(:message, :errors) # FIXME: extract
|
412
|
+
end
|
413
|
+
require "json"
|
414
|
+
class ErrorsRepresenter < Struct.new(:model) # DISCUSS: use Representable?
|
415
|
+
def to_json
|
416
|
+
JSON.generate(errors: model.errors, message: model.message)
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
class DiagramRepresenter < ErrorsRepresenter
|
421
|
+
def to_json
|
422
|
+
JSON.generate({name: model.name})
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
def app_options
|
427
|
+
app_options = {
|
428
|
+
error_representer: ErrorsRepresenter,
|
429
|
+
representer: DiagramRepresenter,
|
430
|
+
errors: Errors.new,
|
431
|
+
}
|
432
|
+
end
|
433
|
+
|
434
|
+
# The idea here is to bridge a FastTrack op (without standardized ends) to the Protocol termini
|
435
|
+
it "LegacyCreate" do
|
436
|
+
# cc_check ==> FailFast
|
437
|
+
ctx = {seq: [], cc_check: false}
|
438
|
+
ctx = {domain_ctx: ctx, **app_options}
|
439
|
+
signal, (ctx, _ ) = Trailblazer::Endpoint.with_or_etc(api_legacy_create_endpoint, [ctx, {}], failure_block: _rails_failure_block)
|
440
|
+
|
441
|
+
signal.inspect.must_equal %{#<Trailblazer::Activity::End semantic=:failure>} # we rewire {domain.fail_fast} to {protocol.failure}
|
442
|
+
ctx[:domain_ctx][:seq].inspect.must_equal %{[:authenticate, :policy, :my_policy, :model, :cc_check]}
|
443
|
+
|
444
|
+
|
445
|
+
|
446
|
+
# 1.c **404** (NO RENDERING OF BODY!!!)
|
447
|
+
ctx = {seq: [], model: false}
|
448
|
+
ctx = {domain_ctx: ctx, **app_options}
|
449
|
+
signal, (ctx, _ ) = Trailblazer::Endpoint.with_or_etc(api_legacy_create_endpoint, [ctx, {}], failure_block: _rails_failure_block)
|
450
|
+
|
451
|
+
signal.inspect.must_equal %{#<Trailblazer::Activity::End semantic=:fail_fast>}
|
452
|
+
ctx[:domain_ctx][:seq].inspect.must_equal %{[:authenticate, :policy, :my_policy, :model]}
|
453
|
+
to_h.inspect.must_equal %{{:render_options=>{:json=>nil, :status=>404}, :failure=>true, :seq=>\"[:authenticate, :policy, :my_policy, :model]\", :signal=>\"#<Trailblazer::Activity::End semantic=:fail_fast>\"}}
|
454
|
+
|
455
|
+
# 2. **201** because the model is new.
|
456
|
+
ctx = {seq: []}
|
457
|
+
ctx = {domain_ctx: ctx, **app_options}
|
458
|
+
signal, (ctx, _ ) = Trailblazer::Endpoint.with_or_etc(api_legacy_create_endpoint, [ctx, {}], success_block: _rails_success_block)
|
459
|
+
|
460
|
+
signal.inspect.must_equal %{#<Trailblazer::Activity::End semantic=:success>}
|
461
|
+
ctx[:domain_ctx][:seq].inspect.must_equal %{[:authenticate, :policy, :my_policy, :model, :cc_check, :validate, :save]}
|
462
|
+
to_h.inspect.must_equal %{{:render_options=>{:json=>\"{\\\"name\\\":\\\"Yo\\\"}\", :status=>201}, :failure=>nil, :seq=>\"[:authenticate, :policy, :my_policy, :model, :cc_check, :validate, :save]\", :signal=>\"#<Trailblazer::Activity::End semantic=:success>\"}}
|
463
|
+
|
464
|
+
# **403** because my_policy fails.
|
465
|
+
ctx = {seq: [], my_policy: false}
|
466
|
+
ctx = {domain_ctx: ctx, **app_options}
|
467
|
+
signal, (ctx, _ ) = Trailblazer::Endpoint.with_or_etc(api_legacy_create_endpoint, [ctx, {}], failure_block: _rails_failure_block)
|
468
|
+
|
469
|
+
signal.inspect.must_equal %{#<Trailblazer::Activity::End semantic=:fail_fast>}
|
470
|
+
ctx[:domain_ctx][:seq].inspect.must_equal %{[:authenticate, :policy, :my_policy]}
|
471
|
+
# this calls Rails default failure block
|
472
|
+
to_h.inspect.must_equal %{{:render_options=>{:json=>\"{\\\"errors\\\":null,\\\"message\\\":\\\"Action not allowed due to a policy.\\\"}\", :status=>403}, :failure=>true, :seq=>\"[:authenticate, :policy, :my_policy]\", :signal=>\"#<Trailblazer::Activity::End semantic=:fail_fast>\"}}
|
473
|
+
# we can read {:domain_activity_return_signal} (currently only set for fails)
|
474
|
+
ctx[:_domain_activity_return_signal].inspect.must_equal %{#<Trailblazer::Activity::End semantic=:failure>}
|
475
|
+
end
|
476
|
+
# TODO: AUTOWIRE RAILWAY/FASTTRACKS
|
477
|
+
|
478
|
+
######### API #########
|
479
|
+
# FIXME: fake the controller
|
480
|
+
let(:_rails_success_block) do ->(ctx, endpoint_ctx:, seq:, signal:, model:, **) {
|
481
|
+
@failure = nil
|
482
|
+
render json: endpoint_ctx[:json], status: endpoint_ctx[:status]; @seq = seq.inspect; @signal = signal.inspect } end
|
483
|
+
let(:_rails_failure_block) do ->(ctx, endpoint_ctx:, seq:, signal:, errors:, **) {
|
484
|
+
@failure = true
|
485
|
+
render json: endpoint_ctx[:json], status: endpoint_ctx[:status]; @seq = seq.inspect; @signal = signal.inspect } end # nil-JSON with 404,
|
486
|
+
|
487
|
+
def render(options)
|
488
|
+
@render_options = options
|
489
|
+
end
|
490
|
+
def to_h
|
491
|
+
{render_options: @render_options, failure: @failure, seq: @seq, signal: @signal}
|
492
|
+
end
|
493
|
+
|
494
|
+
it do
|
495
|
+
puts "API"
|
496
|
+
puts Trailblazer::Developer.render(MyApiAdapter)
|
497
|
+
# puts
|
498
|
+
# puts Trailblazer::Developer.render(Adapter::API::Gemauth)
|
499
|
+
# exit
|
500
|
+
|
501
|
+
|
502
|
+
# 1. ops indicate outcome via termini
|
503
|
+
# 2. you can still "match"
|
504
|
+
# 3. layers
|
505
|
+
# DSL .Or on top
|
506
|
+
# use TRB's wiring API to extend instead of clumsy overriding/super. Example: failure-status
|
507
|
+
|
508
|
+
=begin
|
509
|
+
success
|
510
|
+
representer: Api::V1::Memo::Representer
|
511
|
+
status: 200
|
512
|
+
failure
|
513
|
+
representer: Api::V1::Representer::Error
|
514
|
+
status: 422
|
515
|
+
not_found
|
516
|
+
representer:
|
517
|
+
status: 404
|
518
|
+
|
519
|
+
success_representer: Api::V1::Memo::Representer,
|
520
|
+
failure_representer: Api::V1::Representer::Error,
|
521
|
+
policy: MyPolicy,
|
522
|
+
=end
|
523
|
+
|
524
|
+
|
525
|
+
# api_create_endpoint.instance_exec do
|
526
|
+
|
527
|
+
# step(Subprocess(MyTest::Protocol), patch: {[:protocol] => ->(*) { step :success?, delete: :success? }}, replace: :protocol, inherit: true, id: :protocol) end
|
528
|
+
|
529
|
+
# 1. 401 authenticate err
|
530
|
+
# RENDER an error document
|
531
|
+
ctx = {seq: []}
|
532
|
+
ctx = {domain_ctx: ctx, authenticate: false, **app_options}
|
533
|
+
# signal, (ctx, _ ) = Trailblazer::Developer.wtf?(Adapter::API, [ctx, {}])
|
534
|
+
signal, (ctx, _ ) = Trailblazer::Endpoint.with_or_etc(api_create_endpoint, [ctx, {}], failure_block: _rails_failure_block)
|
535
|
+
|
536
|
+
signal.inspect.must_equal %{#<Trailblazer::Activity::End semantic=:fail_fast>}
|
537
|
+
ctx[:domain_ctx][:seq].inspect.must_equal %{[:authenticate, :my_401_handler]}
|
538
|
+
# DISCUSS: where to add things like headers?
|
539
|
+
# this calls Rails default failure block
|
540
|
+
to_h.inspect.must_equal %{{:render_options=>{:json=>\"{\\\"errors\\\":null,\\\"message\\\":\\\"No token\\\"}\", :status=>401}, :failure=>true, :seq=>\"[:authenticate, :my_401_handler]\", :signal=>\"#<Trailblazer::Activity::End semantic=:fail_fast>\"}}
|
541
|
+
# raise ctx.inspect
|
542
|
+
|
543
|
+
# 1.c 404 (NO RENDERING OF BODY!!!)
|
544
|
+
ctx = {seq: [], model: false}
|
545
|
+
ctx = {domain_ctx: ctx, **app_options}
|
546
|
+
signal, (ctx, _ ) = Trailblazer::Endpoint.with_or_etc(api_create_endpoint, [ctx, {}], failure_block: _rails_failure_block)
|
547
|
+
|
548
|
+
signal.inspect.must_equal %{#<Trailblazer::Activity::End semantic=:fail_fast>}
|
549
|
+
ctx[:domain_ctx][:seq].inspect.must_equal %{[:authenticate, :policy, :model, :handle_not_found]}
|
550
|
+
to_h.inspect.must_equal %{{:render_options=>{:json=>nil, :status=>404}, :failure=>true, :seq=>\"[:authenticate, :policy, :model, :handle_not_found]\", :signal=>\"#<Trailblazer::Activity::End semantic=:fail_fast>\"}}
|
551
|
+
|
552
|
+
# `-- #<Class:0x0000000001ff5d88>
|
553
|
+
# |-- Start.default
|
554
|
+
# |-- protocol
|
555
|
+
# | |-- Start.default
|
556
|
+
# | |-- authenticate
|
557
|
+
# | |-- policy
|
558
|
+
# | |-- domain_activity
|
559
|
+
# | | |-- Start.default
|
560
|
+
# | | |-- model
|
561
|
+
# | | `-- End.not_found
|
562
|
+
# | |-- handle_not_found this is added via the block, in the PROTOCOL wiring
|
563
|
+
# | `-- End.not_found
|
564
|
+
# |-- _404_status
|
565
|
+
# |-- protocol_failure
|
566
|
+
# `-- End.fail_fast
|
567
|
+
|
568
|
+
|
569
|
+
# 1.b 422 domain error: validation failed
|
570
|
+
# RENDER an error document
|
571
|
+
ctx = {seq: [], validate: false}
|
572
|
+
ctx = {domain_ctx: ctx, **app_options}
|
573
|
+
signal, (ctx, _ ) = Trailblazer::Endpoint.with_or_etc(api_create_endpoint, [ctx, {}], failure_block: _rails_failure_block)
|
574
|
+
|
575
|
+
signal.inspect.must_equal %{#<Trailblazer::Activity::End semantic=:failure>}
|
576
|
+
ctx[:domain_ctx][:seq].inspect.must_equal %{[:authenticate, :policy, :model, :cc_check, :validate]}
|
577
|
+
# this calls Rails default failure block
|
578
|
+
to_h.inspect.must_equal %{{:render_options=>{:json=>\"{\\\"errors\\\":null,\\\"message\\\":\\\"The submitted data is invalid.\\\"}\", :status=>422}, :failure=>true, :seq=>\"[:authenticate, :policy, :model, :cc_check, :validate]\", :signal=>\"#<Trailblazer::Activity::End semantic=:failure>\"}}
|
579
|
+
# `-- #<Class:0x0000000002e54e60>
|
580
|
+
# |-- Start.default
|
581
|
+
# |-- protocol
|
582
|
+
# | |-- Start.default
|
583
|
+
# | |-- authenticate
|
584
|
+
# | |-- policy
|
585
|
+
# | |-- domain_activity
|
586
|
+
# | | |-- Start.default
|
587
|
+
# | | |-- model
|
588
|
+
# | | |-- validate
|
589
|
+
# | | `-- End.my_validation_error
|
590
|
+
# | |-- handle_invalid_data
|
591
|
+
# | `-- End.invalid_data this is wired to the {failure} track
|
592
|
+
# |-- failure_render_config
|
593
|
+
# |-- failure_config_status
|
594
|
+
# |-- render_failure
|
595
|
+
# `-- End.failure
|
596
|
+
|
597
|
+
|
598
|
+
|
599
|
+
# 1.b2 another application error (#save), but 200 because of #failure_config_status
|
600
|
+
ctx = {seq: [], save: false}
|
601
|
+
ctx = {domain_ctx: ctx, **app_options}
|
602
|
+
signal, (ctx, _ ) = Trailblazer::Endpoint.with_or_etc(api_create_endpoint, [ctx, {}], failure_block: _rails_failure_block)
|
603
|
+
|
604
|
+
signal.inspect.must_equal %{#<Trailblazer::Activity::End semantic=:failure>}
|
605
|
+
ctx[:domain_ctx][:seq].inspect.must_equal %{[:authenticate, :policy, :model, :cc_check, :validate, :save]}
|
606
|
+
# this calls Rails default failure block
|
607
|
+
# we set status to 200 in #failure_config_status
|
608
|
+
to_h.inspect.must_equal %{{:render_options=>{:json=>\"{\\\"errors\\\":null,\\\"message\\\":null}\", :status=>200}, :failure=>true, :seq=>\"[:authenticate, :policy, :model, :cc_check, :validate, :save]\", :signal=>\"#<Trailblazer::Activity::End semantic=:failure>\"}}
|
609
|
+
|
610
|
+
# invalid {cc_check}=>{cc_invalid}
|
611
|
+
ctx = {seq: [], cc_check: false}
|
612
|
+
ctx = {domain_ctx: ctx, **app_options}
|
613
|
+
signal, (ctx, _ ) = Trailblazer::Endpoint.with_or_etc(api_create_endpoint, [ctx, {}], failure_block: _rails_failure_block)
|
614
|
+
|
615
|
+
signal.inspect.must_equal %{#<Trailblazer::Activity::End semantic=:failure>}
|
616
|
+
ctx[:domain_ctx][:seq].inspect.must_equal %{[:authenticate, :policy, :model, :cc_check]}
|
617
|
+
# this calls Rails default failure block
|
618
|
+
# we set status to 200 in #failure_config_status
|
619
|
+
to_h.inspect.must_equal %{{:render_options=>{:json=>\"{\\\"errors\\\":null,\\\"message\\\":null}\", :status=>200}, :failure=>true, :seq=>\"[:authenticate, :policy, :model, :cc_check]\", :signal=>\"#<Trailblazer::Activity::End semantic=:failure>\"}}
|
620
|
+
|
621
|
+
|
622
|
+
# 4. authorization error
|
623
|
+
ctx = {seq: []}
|
624
|
+
ctx = {policy: false, domain_ctx: ctx, **app_options}
|
625
|
+
signal, (ctx, _ ) = Trailblazer::Endpoint.with_or_etc(api_create_endpoint, [ctx, {}], failure_block: _rails_failure_block)
|
626
|
+
|
627
|
+
signal.inspect.must_equal %{#<Trailblazer::Activity::End semantic=:fail_fast>}
|
628
|
+
ctx[:domain_ctx][:seq].inspect.must_equal %{[:authenticate, :policy]}
|
629
|
+
# this calls Rails default failure block
|
630
|
+
to_h.inspect.must_equal %{{:render_options=>{:json=>\"{\\\"errors\\\":null,\\\"message\\\":\\\"Action not allowed due to a policy.\\\"}\", :status=>403}, :failure=>true, :seq=>\"[:authenticate, :policy]\", :signal=>\"#<Trailblazer::Activity::End semantic=:fail_fast>\"}}
|
631
|
+
|
632
|
+
|
633
|
+
# 2. all OK
|
634
|
+
|
635
|
+
ctx = {seq: []}
|
636
|
+
ctx = {domain_ctx: ctx, **app_options}
|
637
|
+
signal, (ctx, _ ) = Trailblazer::Endpoint.with_or_etc(api_create_endpoint, [ctx, {}], success_block: _rails_success_block)
|
638
|
+
|
639
|
+
|
640
|
+
signal.inspect.must_equal %{#<Trailblazer::Activity::End semantic=:success>}
|
641
|
+
ctx[:domain_ctx][:seq].inspect.must_equal %{[:authenticate, :policy, :model, :cc_check, :validate, :save]}
|
642
|
+
ctx[:json].must_equal %{{\"name\":\"Yo\"}}
|
643
|
+
|
644
|
+
# Rails default success block was called
|
645
|
+
to_h.inspect.must_equal %{{:render_options=>{:json=>\"{\\\"name\\\":\\\"Yo\\\"}\", :status=>200}, :failure=>nil, :seq=>\"[:authenticate, :policy, :model, :cc_check, :validate, :save]\", :signal=>\"#<Trailblazer::Activity::End semantic=:success>\"}}
|
646
|
+
|
647
|
+
|
648
|
+
# 3. 401 for API::Gemauth
|
649
|
+
# we only want to run the authenticate part!
|
650
|
+
#
|
651
|
+
# -- EndpointTest::Adapter::API::Gemauth
|
652
|
+
# |-- Start.default
|
653
|
+
# |-- protocol
|
654
|
+
# | |-- Start.default
|
655
|
+
# | |-- authenticate <this is actually gemserver_authenticate>
|
656
|
+
# | |-- handle_not_authenticated
|
657
|
+
# | `-- End.not_authenticated
|
658
|
+
# |-- my_401_handler
|
659
|
+
# |-- _401_status
|
660
|
+
# |-- render_protocol_failure_config
|
661
|
+
# |-- render_protocol_failure
|
662
|
+
# |-- protocol_failure
|
663
|
+
# `-- End.fail_fast
|
664
|
+
|
665
|
+
|
666
|
+
ctx = {seq: []}
|
667
|
+
ctx = {domain_ctx: ctx, gemserver_authenticate: false, **app_options}
|
668
|
+
signal, (ctx, _ ) = Trailblazer::Endpoint.with_or_etc(Gemauth, [ctx, {}], failure_block: _rails_failure_block)
|
669
|
+
|
670
|
+
signal.inspect.must_equal %{#<Trailblazer::Activity::End semantic=:fail_fast>}
|
671
|
+
ctx[:domain_ctx][:seq].inspect.must_equal %{[:my_401_handler]}
|
672
|
+
to_h.inspect.must_equal %{{:render_options=>{:json=>\"{\\\"errors\\\":null,\\\"message\\\":\\\"No token\\\"}\", :status=>401}, :failure=>true, :seq=>\"[:my_401_handler]\", :signal=>\"#<Trailblazer::Activity::End semantic=:fail_fast>\"}}
|
673
|
+
|
674
|
+
# authentication works
|
675
|
+
# `-- EndpointTest::Adapter::API::Gemauth
|
676
|
+
# |-- Start.default
|
677
|
+
# |-- protocol
|
678
|
+
# | |-- Start.default
|
679
|
+
# | |-- authenticate
|
680
|
+
# | `-- End.success
|
681
|
+
# |-- success_render_config
|
682
|
+
# |-- success_render_status
|
683
|
+
# |-- render_success
|
684
|
+
# `-- End.success
|
685
|
+
|
686
|
+
ctx = {seq: [], gemserver_authenticate: true}
|
687
|
+
ctx = {domain_ctx: ctx, **app_options}
|
688
|
+
|
689
|
+
signal, (ctx, _ ) = Trailblazer::Endpoint.with_or_etc(Gemauth, [ctx, {}], success_block: _rails_success_block)
|
690
|
+
|
691
|
+
signal.inspect.must_equal %{#<Trailblazer::Activity::End semantic=:success>}
|
692
|
+
ctx[:domain_ctx][:seq].inspect.must_equal %{[:gemserver_authenticate]}
|
693
|
+
to_h.inspect.must_equal %{{:render_options=>{:json=>\"{\\\"name\\\":\\\"Gemserver says yes.\\\"}\", :status=>200}, :failure=>nil, :seq=>\"[:gemserver_authenticate]\", :signal=>\"#<Trailblazer::Activity::End semantic=:success>\"}}
|
694
|
+
|
695
|
+
|
696
|
+
######### Controller #########
|
697
|
+
|
698
|
+
# 1. do everything automatically
|
699
|
+
# 2. override success
|
700
|
+
# 2. override failure: suppress the automatic rendering?
|
701
|
+
|
702
|
+
|
703
|
+
# class MyPolicyChain < Trailblazer::Endpoint::PolicyChain
|
704
|
+
# step :a?
|
705
|
+
# step :b?
|
706
|
+
# end
|
707
|
+
|
708
|
+
# Trailblazer::Endpoint(policy: MyPolicyChain)
|
709
|
+
|
710
|
+
# class MyEndpoint < Trailblazer::Endpoint
|
711
|
+
# step MyPolicies, replace: :policy # with or without root, we have a binary outcome?
|
712
|
+
# end
|
713
|
+
|
714
|
+
# MyEndpoint.() # with operation
|
715
|
+
# MyEndpoint.() # with workflow
|
716
|
+
|
717
|
+
# # op with > 2 ends
|
718
|
+
# {our_404: :not_found} # map ends to known ends
|
719
|
+
|
720
|
+
# # html version
|
721
|
+
# run(MyEndpoint, ) do |ctx|
|
722
|
+
# # success
|
723
|
+
# end
|
724
|
+
|
725
|
+
# # api version
|
726
|
+
# # if 404 ...
|
727
|
+
# # else default behavior
|
728
|
+
# end
|
729
|
+
|
730
|
+
|
731
|
+
|
732
|
+
|
733
|
+
end
|
734
|
+
end
|
735
|
+
|
736
|
+
# require "test_helper"
|
737
|
+
|
738
|
+
# require "reform"
|
739
|
+
# require "trailblazer"
|
740
|
+
# require "reform/form/dry"
|
741
|
+
# require "trailblazer/endpoint"
|
742
|
+
# require "trailblazer/endpoint/rails"
|
743
|
+
|
744
|
+
# class EndpointTest < Minitest::Spec
|
745
|
+
# Song = Struct.new(:id, :title, :length) do
|
746
|
+
# def self.find_by(id:nil); id.nil? ? nil : new(id) end
|
747
|
+
# end
|
748
|
+
|
749
|
+
# require "representable/json"
|
750
|
+
# class Serializer < Representable::Decorator
|
751
|
+
# include Representable::JSON
|
752
|
+
# property :id
|
753
|
+
# property :title
|
754
|
+
# property :length
|
755
|
+
|
756
|
+
# class Errors < Representable::Decorator
|
757
|
+
# include Representable::JSON
|
758
|
+
# property :messages
|
759
|
+
# end
|
760
|
+
# end
|
761
|
+
|
762
|
+
# class Deserializer < Representable::Decorator
|
763
|
+
# include Representable::JSON
|
764
|
+
# property :title
|
765
|
+
# end
|
766
|
+
|
767
|
+
# let (:my_handlers) {
|
768
|
+
# ->(m) do
|
769
|
+
# m.present { |result| _data << result["representer.serializer.class"].new(result["model"]).to_json }
|
770
|
+
# end
|
771
|
+
# }
|
772
|
+
|
773
|
+
# #---
|
774
|
+
# # present
|
775
|
+
# class Show < Trailblazer::Operation
|
776
|
+
# extend Representer::DSL
|
777
|
+
# step Model( Song, :find_by )
|
778
|
+
# representer :serializer, Serializer
|
779
|
+
# end
|
780
|
+
|
781
|
+
# # if you pass in "present"=>true as a dependency, the Endpoint will understand it's a present cycle.
|
782
|
+
# it do
|
783
|
+
# Trailblazer::Endpoint.new.(Show.({ id: 1 }, { "present" => true }), my_handlers)
|
784
|
+
# _data.must_equal ['{"id":1}']
|
785
|
+
# end
|
786
|
+
|
787
|
+
# # passing handlers directly to Endpoint#call.
|
788
|
+
# it do
|
789
|
+
# result = Show.({ id: 1 }, { "present" => true })
|
790
|
+
# Trailblazer::Endpoint.new.(result) do |m|
|
791
|
+
# m.present { |result| _data << result["representer.serializer.class"].new(result["model"]).to_json }
|
792
|
+
# end
|
793
|
+
|
794
|
+
# _data.must_equal ['{"id":1}']
|
795
|
+
# end
|
796
|
+
|
797
|
+
|
798
|
+
# class Create < Trailblazer::Operation
|
799
|
+
# step Policy::Guard ->(options) { options["user.current"] == ::Module }
|
800
|
+
|
801
|
+
# extend Representer::DSL
|
802
|
+
# representer :serializer, Serializer
|
803
|
+
# representer :deserializer, Deserializer
|
804
|
+
# representer :errors, Serializer::Errors
|
805
|
+
# # self["representer.serializer.class"] = Representer
|
806
|
+
# # self["representer.deserializer.class"] = Deserializer
|
807
|
+
|
808
|
+
|
809
|
+
# extend Contract::DSL
|
810
|
+
# contract do
|
811
|
+
# property :title
|
812
|
+
# property :length
|
813
|
+
|
814
|
+
# include Reform::Form::Dry
|
815
|
+
# validation :default do
|
816
|
+
# required(:title).filled
|
817
|
+
# end
|
818
|
+
# end
|
819
|
+
|
820
|
+
# step Model( Song, :new )
|
821
|
+
# step Contract::Build()
|
822
|
+
# step Contract::Validate( representer: self["representer.deserializer.class"] )
|
823
|
+
# step Persist( method: :sync )
|
824
|
+
# step ->(options) { options["model"].id = 9 }
|
825
|
+
# end
|
826
|
+
|
827
|
+
# let (:controller) { self }
|
828
|
+
# let (:_data) { [] }
|
829
|
+
# def head(*args); _data << [:head, *args] end
|
830
|
+
|
831
|
+
# let(:handlers) { Trailblazer::Endpoint::Handlers::Rails.new(self, path: "/songs").() }
|
832
|
+
# def render(options)
|
833
|
+
# _data << options
|
834
|
+
# end
|
835
|
+
# # not authenticated, 401
|
836
|
+
# it do
|
837
|
+
# result = Create.( { id: 1 }, "user.current" => false )
|
838
|
+
# # puts "@@@@@ #{result.inspect}"
|
839
|
+
|
840
|
+
# Trailblazer::Endpoint.new.(result, handlers)
|
841
|
+
# _data.inspect.must_equal %{[[:head, 401]]}
|
842
|
+
# end
|
843
|
+
|
844
|
+
# # created
|
845
|
+
# # length is ignored as it's not defined in the deserializer.
|
846
|
+
# it do
|
847
|
+
# result = Create.( {}, "user.current" => ::Module, "document" => '{"id": 9, "title": "Encores", "length": 999 }' )
|
848
|
+
# # puts "@@@@@ #{result.inspect}"
|
849
|
+
|
850
|
+
# Trailblazer::Endpoint.new.(result, handlers)
|
851
|
+
# _data.inspect.must_equal '[[:head, 201, {:location=>"/songs/9"}]]'
|
852
|
+
# end
|
853
|
+
|
854
|
+
# class Update < Create
|
855
|
+
# self.~ Model( :find_by )
|
856
|
+
# end
|
857
|
+
|
858
|
+
# # 404
|
859
|
+
# it do
|
860
|
+
# result = Update.({ id: nil }, "user.current" => ::Module, "document" => '{"id": 9, "title": "Encores", "length": 999 }' )
|
861
|
+
|
862
|
+
# Trailblazer::Endpoint.new.(result, handlers)
|
863
|
+
# _data.inspect.must_equal '[[:head, 404]]'
|
864
|
+
# end
|
865
|
+
|
866
|
+
# #---
|
867
|
+
# # validation failure 422
|
868
|
+
# # success
|
869
|
+
# it do
|
870
|
+
# result = Create.({}, "user.current" => ::Module, "document" => '{ "title": "" }')
|
871
|
+
# Trailblazer::Endpoint.new.(result, handlers)
|
872
|
+
# _data.inspect.must_equal '[{:json=>"{\\"messages\\":{\\"title\\":[\\"must be filled\\"]}}", :status=>422}]'
|
873
|
+
# end
|
874
|
+
|
875
|
+
|
876
|
+
# include Trailblazer::Endpoint::Controller
|
877
|
+
# #---
|
878
|
+
# # Controller#endpoint
|
879
|
+
# # custom handler.
|
880
|
+
# it do
|
881
|
+
# invoked = nil
|
882
|
+
|
883
|
+
# endpoint(Update, { id: nil }) do |res|
|
884
|
+
# res.not_found { invoked = "my not_found!" }
|
885
|
+
# end
|
886
|
+
|
887
|
+
# invoked.must_equal "my not_found!"
|
888
|
+
# _data.must_equal [] # no rails code involved.
|
889
|
+
# end
|
890
|
+
|
891
|
+
# # generic handler because user handler doesn't match.
|
892
|
+
# it do
|
893
|
+
# invoked = nil
|
894
|
+
|
895
|
+
# endpoint( Update, { id: nil }, args: {"user.current" => ::Module} ) do |res|
|
896
|
+
# res.invalid { invoked = "my invalid!" }
|
897
|
+
# end
|
898
|
+
|
899
|
+
# _data.must_equal [[:head, 404]]
|
900
|
+
# invoked.must_equal nil
|
901
|
+
# end
|
902
|
+
|
903
|
+
# # only generic handler
|
904
|
+
# it do
|
905
|
+
# endpoint(Update, { id: nil })
|
906
|
+
# _data.must_equal [[:head, 404]]
|
907
|
+
# end
|
908
|
+
# end
|