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.
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