trailblazer-endpoint 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGES.md +3 -0
  3. data/Gemfile +16 -0
  4. data/README.md +11 -0
  5. data/Rakefile +10 -0
  6. data/lib/trailblazer-endpoint.rb +1 -0
  7. data/lib/trailblazer/endpoint.rb +133 -0
  8. data/lib/trailblazer/endpoint/adapter.rb +197 -0
  9. data/lib/trailblazer/endpoint/builder.rb +56 -0
  10. data/lib/trailblazer/endpoint/protocol.rb +122 -0
  11. data/lib/trailblazer/endpoint/rails.rb +27 -0
  12. data/lib/trailblazer/endpoint/version.rb +5 -0
  13. data/test/adapter/api_test.rb +78 -0
  14. data/test/adapter/representable_test.rb +7 -0
  15. data/test/adapter/web_test.rb +40 -0
  16. data/test/benchmark/skill_resolver_benchmark.rb +43 -0
  17. data/test/docs/controller_test.rb +92 -0
  18. data/test/docs/endpoint_test.rb +54 -0
  19. data/test/endpoint_test.rb +908 -0
  20. data/test/rails-app/.gitignore +21 -0
  21. data/test/rails-app/Gemfile +17 -0
  22. data/test/rails-app/Gemfile.lock +157 -0
  23. data/test/rails-app/README.md +24 -0
  24. data/test/rails-app/Rakefile +6 -0
  25. data/test/rails-app/app/controllers/application_controller.rb +3 -0
  26. data/test/rails-app/app/controllers/songs_controller.rb +26 -0
  27. data/test/rails-app/app/models/application_record.rb +3 -0
  28. data/test/rails-app/app/models/concerns/.keep +0 -0
  29. data/test/rails-app/config.ru +5 -0
  30. data/test/rails-app/config/application.rb +15 -0
  31. data/test/rails-app/config/boot.rb +3 -0
  32. data/test/rails-app/config/database.yml +25 -0
  33. data/test/rails-app/config/environment.rb +5 -0
  34. data/test/rails-app/config/environments/development.rb +54 -0
  35. data/test/rails-app/config/environments/production.rb +86 -0
  36. data/test/rails-app/config/environments/test.rb +42 -0
  37. data/test/rails-app/config/initializers/application_controller_renderer.rb +6 -0
  38. data/test/rails-app/config/initializers/backtrace_silencers.rb +7 -0
  39. data/test/rails-app/config/initializers/cookies_serializer.rb +5 -0
  40. data/test/rails-app/config/initializers/filter_parameter_logging.rb +4 -0
  41. data/test/rails-app/config/initializers/inflections.rb +16 -0
  42. data/test/rails-app/config/initializers/mime_types.rb +4 -0
  43. data/test/rails-app/config/initializers/new_framework_defaults.rb +24 -0
  44. data/test/rails-app/config/initializers/session_store.rb +3 -0
  45. data/test/rails-app/config/initializers/wrap_parameters.rb +14 -0
  46. data/test/rails-app/config/locales/en.yml +23 -0
  47. data/test/rails-app/config/routes.rb +6 -0
  48. data/test/rails-app/config/secrets.yml +22 -0
  49. data/test/rails-app/db/seeds.rb +7 -0
  50. data/test/rails-app/log/.keep +0 -0
  51. data/test/rails-app/test/controllers/.keep +0 -0
  52. data/test/rails-app/test/controllers/songs_controller_test.rb +156 -0
  53. data/test/rails-app/test/fixtures/.keep +0 -0
  54. data/test/rails-app/test/fixtures/files/.keep +0 -0
  55. data/test/rails-app/test/helpers/.keep +0 -0
  56. data/test/rails-app/test/integration/.keep +0 -0
  57. data/test/rails-app/test/mailers/.keep +0 -0
  58. data/test/rails-app/test/models/.keep +0 -0
  59. data/test/rails-app/test/test_helper.rb +10 -0
  60. data/test/rails-app/tmp/.keep +0 -0
  61. data/test/rails-app/vendor/assets/javascripts/.keep +0 -0
  62. data/test/rails-app/vendor/assets/stylesheets/.keep +0 -0
  63. data/test/test_helper.rb +34 -0
  64. data/trailblazer-endpoint.gemspec +26 -0
  65. 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,5 @@
1
+ module Trailblazer
2
+ class Endpoint
3
+ VERSION = "0.0.1"
4
+ end
5
+ 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,7 @@
1
+ require "test_helper"
2
+
3
+ class AdapterRepresentableTest < Minitest::Spec
4
+ it "what" do
5
+ raise "please implement Adapter::API::Representable"
6
+ end
7
+ 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