trailblazer-endpoint 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGES.md +3 -0
- data/Gemfile +16 -0
- data/README.md +11 -0
- data/Rakefile +10 -0
- data/lib/trailblazer-endpoint.rb +1 -0
- data/lib/trailblazer/endpoint.rb +133 -0
- data/lib/trailblazer/endpoint/adapter.rb +197 -0
- data/lib/trailblazer/endpoint/builder.rb +56 -0
- data/lib/trailblazer/endpoint/protocol.rb +122 -0
- data/lib/trailblazer/endpoint/rails.rb +27 -0
- data/lib/trailblazer/endpoint/version.rb +5 -0
- data/test/adapter/api_test.rb +78 -0
- data/test/adapter/representable_test.rb +7 -0
- data/test/adapter/web_test.rb +40 -0
- data/test/benchmark/skill_resolver_benchmark.rb +43 -0
- data/test/docs/controller_test.rb +92 -0
- data/test/docs/endpoint_test.rb +54 -0
- data/test/endpoint_test.rb +908 -0
- data/test/rails-app/.gitignore +21 -0
- data/test/rails-app/Gemfile +17 -0
- data/test/rails-app/Gemfile.lock +157 -0
- data/test/rails-app/README.md +24 -0
- data/test/rails-app/Rakefile +6 -0
- data/test/rails-app/app/controllers/application_controller.rb +3 -0
- data/test/rails-app/app/controllers/songs_controller.rb +26 -0
- data/test/rails-app/app/models/application_record.rb +3 -0
- data/test/rails-app/app/models/concerns/.keep +0 -0
- data/test/rails-app/config.ru +5 -0
- data/test/rails-app/config/application.rb +15 -0
- data/test/rails-app/config/boot.rb +3 -0
- data/test/rails-app/config/database.yml +25 -0
- data/test/rails-app/config/environment.rb +5 -0
- data/test/rails-app/config/environments/development.rb +54 -0
- data/test/rails-app/config/environments/production.rb +86 -0
- data/test/rails-app/config/environments/test.rb +42 -0
- data/test/rails-app/config/initializers/application_controller_renderer.rb +6 -0
- data/test/rails-app/config/initializers/backtrace_silencers.rb +7 -0
- data/test/rails-app/config/initializers/cookies_serializer.rb +5 -0
- data/test/rails-app/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/rails-app/config/initializers/inflections.rb +16 -0
- data/test/rails-app/config/initializers/mime_types.rb +4 -0
- data/test/rails-app/config/initializers/new_framework_defaults.rb +24 -0
- data/test/rails-app/config/initializers/session_store.rb +3 -0
- data/test/rails-app/config/initializers/wrap_parameters.rb +14 -0
- data/test/rails-app/config/locales/en.yml +23 -0
- data/test/rails-app/config/routes.rb +6 -0
- data/test/rails-app/config/secrets.yml +22 -0
- data/test/rails-app/db/seeds.rb +7 -0
- data/test/rails-app/log/.keep +0 -0
- data/test/rails-app/test/controllers/.keep +0 -0
- data/test/rails-app/test/controllers/songs_controller_test.rb +156 -0
- data/test/rails-app/test/fixtures/.keep +0 -0
- data/test/rails-app/test/fixtures/files/.keep +0 -0
- data/test/rails-app/test/helpers/.keep +0 -0
- data/test/rails-app/test/integration/.keep +0 -0
- data/test/rails-app/test/mailers/.keep +0 -0
- data/test/rails-app/test/models/.keep +0 -0
- data/test/rails-app/test/test_helper.rb +10 -0
- data/test/rails-app/tmp/.keep +0 -0
- data/test/rails-app/vendor/assets/javascripts/.keep +0 -0
- data/test/rails-app/vendor/assets/stylesheets/.keep +0 -0
- data/test/test_helper.rb +34 -0
- data/trailblazer-endpoint.gemspec +26 -0
- metadata +236 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a13cc4ea9b50a656f620578a75ccd11aa01106bc22be8975e8758a660d1a13aa
|
4
|
+
data.tar.gz: 4c006bbfeab10458615ee658e862cf152b42b17f1c0d8302f9d1308b8cdc1425
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 53dfeec3df63a69bbe4f481f58eaa6c71e32d55044066fda4eb7dc8579ef1b973079ff8fa6b7cbd7a6f4ce4ecdb767d25f37a6bf68eee99626557f1a1de2ab71
|
7
|
+
data.tar.gz: 99b9e753a78f182ad274c677aa9f1c33bc85fbd8ad4ce57c2574a4b4ef29d3400e3553c6484674e3d75cec41dcacea76f601db77aee7273fdd066ec3c41c17c7
|
data/CHANGES.md
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in trailblazer.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem "multi_json"
|
7
|
+
|
8
|
+
gem "minitest-line"
|
9
|
+
|
10
|
+
# gem "trailblazer-activity", path: "../trailblazer-activity"
|
11
|
+
# gem "trailblazer-activity-dsl-linear", path: "../trailblazer-activity-dsl-linear"
|
12
|
+
# gem "trailblazer-operation", path: "../operation"
|
13
|
+
|
14
|
+
gem "dry-validation"
|
15
|
+
|
16
|
+
# gem "trailblazer-developer", path: "../trailblazer-developer"
|
data/README.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Trailblazer::Endpoint
|
2
|
+
|
3
|
+
*Generic HTTP handlers for operation results.*
|
4
|
+
|
5
|
+
Decouple finding out *what happened* from *what to do*.
|
6
|
+
|
7
|
+
t test/controllers/songs_controller_test.rb --backtrace
|
8
|
+
|
9
|
+
## TODO
|
10
|
+
|
11
|
+
* make travis build run `cd test/rails-app/ && rake`
|
data/Rakefile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
3
|
+
|
4
|
+
task :default => [:test]
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |test|
|
7
|
+
test.libs << 'test'
|
8
|
+
test.test_files = FileList['test/endpoint_test.rb', 'test/docs/*_test.rb', "test/adapter/*_test.rb"]
|
9
|
+
test.verbose = true
|
10
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require "trailblazer/endpoint"
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
class Endpoint
|
3
|
+
# Create an {Endpoint} class with the provided adapter and protocol.
|
4
|
+
# This builder also sets up taskWrap filters around the {domain_activity} execution.
|
5
|
+
def self.build(protocol:, adapter:, domain_activity:, scope_domain_ctx: true, domain_ctx_filter: nil, &block)
|
6
|
+
|
7
|
+
# special considerations around the {domain_activity} and its taskWrap:
|
8
|
+
#
|
9
|
+
# 1. domain_ctx_filter (e.g. to filter {current_user})
|
10
|
+
# 2. :input (scope {:domain_ctx})
|
11
|
+
# 3. call (domain_activity)
|
12
|
+
# 4. :output
|
13
|
+
# 5. save return signal
|
14
|
+
|
15
|
+
|
16
|
+
extensions_options = {
|
17
|
+
extensions: [Trailblazer::Activity::TaskWrap::Extension(merge: Trailblazer::Endpoint::Protocol::Domain.extension_for_terminus_handler)],
|
18
|
+
}
|
19
|
+
|
20
|
+
# scoping: {:domain_ctx} becomes ctx
|
21
|
+
extensions_options.merge!(Endpoint.options_for_scope_domain_ctx) if scope_domain_ctx # TODO: test flag
|
22
|
+
|
23
|
+
|
24
|
+
domain_ctx_filter_callable = [[Trailblazer::Activity::TaskWrap::Pipeline.method(:insert_before), "task_wrap.call_task", ["endpoint.domain_ctx_filter", domain_ctx_filter]]]
|
25
|
+
extensions_options[:extensions] << Trailblazer::Activity::TaskWrap::Extension(merge: domain_ctx_filter_callable) if domain_ctx_filter
|
26
|
+
|
27
|
+
app_protocol = Class.new(protocol) do
|
28
|
+
step(Subprocess(domain_activity), {inherit: true, id: :domain_activity, replace: :domain_activity,
|
29
|
+
|
30
|
+
# FIXME: where does this go?
|
31
|
+
}.
|
32
|
+
merge(extensions_options).
|
33
|
+
merge(instance_exec(&block)) # the block is evaluated in the {Protocol} context.
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
Class.new(adapter) do
|
38
|
+
step(Subprocess(app_protocol), {inherit: true, id: :protocol, replace: :protocol})
|
39
|
+
end # app_adapter
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.options_for_scope_domain_ctx()
|
44
|
+
{
|
45
|
+
input: ->(ctx, **) { ctx[:domain_ctx] }, # gets automatically Context()'ed.
|
46
|
+
output: ->(domain_ctx, **) { {:domain_ctx => domain_ctx} }
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
# Runtime
|
51
|
+
# Invokes the endpoint for you and runs one of the three outcome blocks.
|
52
|
+
def self.with_or_etc(activity, args, failure_block:, success_block:, protocol_failure_block:)
|
53
|
+
# args[1] = args[1].merge(focus_on: { variables: [:returned], steps: :invoke_workflow })
|
54
|
+
|
55
|
+
signal, (endpoint_ctx, _ ) = Trailblazer::Developer.wtf?(activity, args)
|
56
|
+
|
57
|
+
# this ctx is passed to the controller block.
|
58
|
+
block_ctx = endpoint_ctx[:domain_ctx].merge(endpoint_ctx: endpoint_ctx, signal: signal, errors: endpoint_ctx[:errors]) # DISCUSS: errors? status?
|
59
|
+
|
60
|
+
# if signal < Trailblazer::Activity::End::Success
|
61
|
+
adapter_terminus_semantic = signal.to_h[:semantic]
|
62
|
+
|
63
|
+
executed_block =
|
64
|
+
if adapter_terminus_semantic == :success
|
65
|
+
success_block
|
66
|
+
elsif adapter_terminus_semantic == :fail_fast
|
67
|
+
protocol_failure_block
|
68
|
+
else
|
69
|
+
failure_block
|
70
|
+
end
|
71
|
+
|
72
|
+
executed_block.(block_ctx, **block_ctx)
|
73
|
+
|
74
|
+
# we return the original context???
|
75
|
+
return signal, [endpoint_ctx]
|
76
|
+
end
|
77
|
+
|
78
|
+
# def self.default_success_if(success_id)
|
79
|
+
# ->(signal:, graph:, **) { signal[:lane_positions][suspend_activity].last == graph.find(success_id).task }
|
80
|
+
# end
|
81
|
+
|
82
|
+
#@ For WORKFLOW and operations. not sure this method will stay here.
|
83
|
+
def self.arguments_for(domain_ctx:, collaboration:, dictionary: collaboration.to_h[:dictionary], flow_options:, circuit_options: {}, **options)
|
84
|
+
domain_ctx = Trailblazer::Context::IndifferentAccess.build(domain_ctx, {}, [domain_ctx, flow_options], circuit_options)
|
85
|
+
|
86
|
+
[
|
87
|
+
[
|
88
|
+
{
|
89
|
+
activity: collaboration,
|
90
|
+
domain_ctx: domain_ctx, # DISCUSS: is this where {:resume_data} comes in?
|
91
|
+
# process_model_class: process_model_class,
|
92
|
+
# process_model_from_resume_data: process_model_from_resume_data,
|
93
|
+
# find_process_model: find_process_model,
|
94
|
+
# encrypted_resume_data: encrypted_resume_data,
|
95
|
+
|
96
|
+
dictionary: dictionary,
|
97
|
+
# cipher_key: cipher_key,
|
98
|
+
**options,
|
99
|
+
},
|
100
|
+
flow_options
|
101
|
+
],
|
102
|
+
circuit_options
|
103
|
+
]
|
104
|
+
end
|
105
|
+
|
106
|
+
# FIXME: name will change! this is for controllers, only!
|
107
|
+
def self.advance_from_controller(endpoint, success_block:, failure_block:, protocol_failure_block: protocol_failure_block, **argument_options)
|
108
|
+
args = Trailblazer::Endpoint.arguments_for(argument_options)
|
109
|
+
|
110
|
+
signal, (ctx, _ ) = Trailblazer::Endpoint.with_or_etc(
|
111
|
+
endpoint,
|
112
|
+
args[0], # [ctx, flow_options]
|
113
|
+
|
114
|
+
success_block: success_block,
|
115
|
+
failure_block: failure_block,
|
116
|
+
protocol_failure_block: protocol_failure_block,
|
117
|
+
)
|
118
|
+
|
119
|
+
ctx
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
# created: Dry::Matcher::Case.new(
|
124
|
+
# match: ->(result) { result.success? && result["model.action"] == :new }, # the "model.action" doesn't mean you need Model.
|
125
|
+
# resolve: ->(result) { result }),
|
126
|
+
# not_found: Dry::Matcher::Case.new(
|
127
|
+
# match: ->(result) { result.failure? && result["result.model"] && result["result.model"].failure? },
|
128
|
+
# resolve: ->(result) { result }),
|
129
|
+
# # TODO: we could add unauthorized here.
|
130
|
+
|
131
|
+
|
132
|
+
require "trailblazer/endpoint/protocol"
|
133
|
+
require "trailblazer/endpoint/adapter"
|
@@ -0,0 +1,197 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
class Endpoint
|
3
|
+
|
4
|
+
# The idea is to use the CreatePrototypeProtocol's outputs as some kind of protocol, outcomes that need special handling
|
5
|
+
# can be wired here, or merged into one (e.g. 401 and failure is failure).
|
6
|
+
# I am writing this class in the deep forests of the Algarve, hiding from the GNR.
|
7
|
+
# class Adapter < Trailblazer::Activity::FastTrack # TODO: naming. it's after the "application logic", more like Controller
|
8
|
+
# Currently reusing End.fail_fast as a "something went wrong, but it wasn't a real application error!"
|
9
|
+
|
10
|
+
|
11
|
+
module Adapter
|
12
|
+
class Web <Trailblazer::Activity::FastTrack
|
13
|
+
_404_path = ->(*) { step :_404_status }
|
14
|
+
_401_path = ->(*) { step :_401_status }
|
15
|
+
_403_path = ->(*) { step :_403_status }
|
16
|
+
# _422_path = ->(*) { step :_422_status } # TODO: this is currently represented by the {failure} track.
|
17
|
+
|
18
|
+
step Subprocess(Protocol), # this will get replaced
|
19
|
+
id: :protocol,
|
20
|
+
Output(:not_authorized) => Path(track_color: :not_authorized, connect_to: Id(:protocol_failure), &_403_path),
|
21
|
+
Output(:not_found) => Path(track_color: :not_found, connect_to: Id(:protocol_failure), &_404_path),
|
22
|
+
Output(:not_authenticated) => Path(track_color: :not_authenticated, connect_to: Id(:protocol_failure), &_401_path),
|
23
|
+
Output(:invalid_data) => Track(:failure) # application error, since it's usually a failed validation.
|
24
|
+
|
25
|
+
step :protocol_failure, magnetic_to: nil, Output(:success) => Track(:fail_fast), Output(:failure) => Track(:fail_fast)
|
26
|
+
|
27
|
+
def protocol_failure(ctx, **)
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
# FIXME:::::::
|
33
|
+
def _401_status(ctx, **)
|
34
|
+
ctx[:status] = 401
|
35
|
+
end
|
36
|
+
|
37
|
+
def _404_status(ctx, **)
|
38
|
+
ctx[:status] = 404
|
39
|
+
end
|
40
|
+
|
41
|
+
def _403_status(ctx, **)
|
42
|
+
ctx[:status] = 403
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class API < Web
|
47
|
+
step :_200_status, after: :protocol
|
48
|
+
|
49
|
+
def _200_status(ctx, **)
|
50
|
+
ctx[:status] = 200
|
51
|
+
end
|
52
|
+
|
53
|
+
fail :_422_status, before: "End.failure"
|
54
|
+
|
55
|
+
def _422_status(ctx, **)
|
56
|
+
ctx[:status] = 422
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
def self.insert_error_handler_steps(adapter)
|
61
|
+
adapter = Class.new(adapter) do
|
62
|
+
step :handle_not_authenticated, magnetic_to: :not_authenticated, Output(:success) => Track(:not_authenticated), Output(:failure) => Track(:not_authenticated), before: :_401_status
|
63
|
+
step :handle_not_authorized, magnetic_to: :not_authorized, Output(:success) => Track(:not_authorized), Output(:failure) => Track(:not_authorized), before: :_403_status
|
64
|
+
# step :handle_not_found, magnetic_to: :not_found, Output(:success) => Track(:not_found), Output(:failure) => Track(:not_found)
|
65
|
+
fail :handle_invalid_data
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Errors < Struct.new(:message, :errors) # FIXME: extract
|
70
|
+
module Handlers
|
71
|
+
def handle_not_authenticated(ctx, errors:, **)
|
72
|
+
errors.message = "Authentication credentials were not provided or are invalid."
|
73
|
+
end
|
74
|
+
|
75
|
+
def handle_not_authorized(ctx, errors:, **)
|
76
|
+
errors.message = "Action not allowed due to a policy setting."
|
77
|
+
end
|
78
|
+
|
79
|
+
def handle_invalid_data(ctx, errors:, **)
|
80
|
+
errors.message = "The submitted data is invalid."
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Basic endpoint adapter for a HTTP document API.
|
87
|
+
# As always: "work in progress" ;)
|
88
|
+
#
|
89
|
+
# {End.fail_fast} currently implies a 4xx-able error.
|
90
|
+
class API_ < Trailblazer::Activity::FastTrack
|
91
|
+
_404_path = ->(*) { step :_404_status }
|
92
|
+
_401_path = ->(*) { step :_401_status; step :_401_error_message }
|
93
|
+
_403_path = ->(*) { step :_403_status }
|
94
|
+
# _422_path = ->(*) { step :_422_status } # TODO: this is currently represented by the {failure} track.
|
95
|
+
|
96
|
+
# The API Adapter automatically wires well-defined outputs for you to well-defined paths. :)
|
97
|
+
# FIXME
|
98
|
+
|
99
|
+
step Subprocess(Protocol), # this will get replaced
|
100
|
+
id: :protocol,
|
101
|
+
Output(:not_authorized) => Path(track_color: :_403, connect_to: Id(:render_protocol_failure_config), &_403_path),
|
102
|
+
Output(:not_found) => Path(track_color: :_404, connect_to: Id(:protocol_failure), &_404_path),
|
103
|
+
Output(:not_authenticated) => Path(track_color: :_401, connect_to: Id(:render_protocol_failure_config), &_401_path), # head(401), representer: Representer::Error, message: no token
|
104
|
+
Output(:invalid_data) => Track(:failure) # application error, since it's usually a failed validation.
|
105
|
+
|
106
|
+
# extensions: [Trailblazer::Activity::TaskWrap::Extension(merge: TERMINUS_HANDLER)]
|
107
|
+
# failure is automatically wired to failure, being an "application error" vs. a "protocol error (auth, etc)"
|
108
|
+
|
109
|
+
|
110
|
+
fail :failure_render_config
|
111
|
+
fail :failure_config_status
|
112
|
+
fail :render_failure
|
113
|
+
|
114
|
+
step :success_render_config
|
115
|
+
step :success_render_status
|
116
|
+
step :render_success
|
117
|
+
|
118
|
+
|
119
|
+
# DISCUSS: "protocol failure" and "application failure" should be the same path, probably?
|
120
|
+
step :render_protocol_failure_config, magnetic_to: nil, Output(:success) => Path(connect_to: Id("End.fail_fast")) do
|
121
|
+
step :render_protocol_failure
|
122
|
+
step :protocol_failure
|
123
|
+
end
|
124
|
+
|
125
|
+
=begin
|
126
|
+
render_protocol_failure_config # representer
|
127
|
+
render_protocol_failure # Representer.new
|
128
|
+
protocol_failure # true
|
129
|
+
=end
|
130
|
+
|
131
|
+
def success_render_status(ctx, **)
|
132
|
+
ctx[:status] = 200
|
133
|
+
end
|
134
|
+
|
135
|
+
def success_render_config(ctx, representer:, **)
|
136
|
+
true
|
137
|
+
end
|
138
|
+
|
139
|
+
def render_protocol_failure_config(*args)
|
140
|
+
failure_render_config(*args)
|
141
|
+
end
|
142
|
+
|
143
|
+
# ROAR
|
144
|
+
def render_success(ctx, representer:, domain_ctx:, **)
|
145
|
+
model = domain_ctx[:model]
|
146
|
+
ctx[:json] = representer.new(model).to_json # FIXME: use the same as render_failure.
|
147
|
+
end
|
148
|
+
|
149
|
+
def failure_render_config(ctx, error_representer:, **)
|
150
|
+
ctx[:representer] = error_representer
|
151
|
+
end
|
152
|
+
|
153
|
+
def failure_config_status(ctx, **)
|
154
|
+
ctx[:status] = 422
|
155
|
+
end
|
156
|
+
|
157
|
+
def protocol_failure(*args)
|
158
|
+
#failure_config(*args)
|
159
|
+
true
|
160
|
+
end
|
161
|
+
def render_protocol_failure(*args)
|
162
|
+
render_failure(*args)
|
163
|
+
end
|
164
|
+
|
165
|
+
# ROAR
|
166
|
+
def render_failure(ctx, error_representer:, errors:, **)
|
167
|
+
# render_success(*args)
|
168
|
+
ctx[:json] = error_representer.new(errors).to_json
|
169
|
+
end
|
170
|
+
# how/where would we configure each endpoint? (per action)
|
171
|
+
# class Endpoint
|
172
|
+
# representer ...
|
173
|
+
# message ...
|
174
|
+
|
175
|
+
def _401_status(ctx, **)
|
176
|
+
ctx[:status] = 401
|
177
|
+
end
|
178
|
+
|
179
|
+
def _404_status(ctx, **)
|
180
|
+
ctx[:status] = 404
|
181
|
+
end
|
182
|
+
|
183
|
+
def _403_status(ctx, **)
|
184
|
+
ctx[:status] = 403
|
185
|
+
end
|
186
|
+
|
187
|
+
def _401_error_message(ctx, **)
|
188
|
+
ctx[:error_message] = "Authentication credentials were not provided or invalid."
|
189
|
+
end
|
190
|
+
|
191
|
+
# def exec_success(ctx, success_block:, **)
|
192
|
+
# success_block.call(ctx, **ctx.to_hash) # DISCUSS: use Nested(dynamic) ?
|
193
|
+
# end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
class Endpoint
|
3
|
+
# you don't need this if you build your endpoints manually
|
4
|
+
class Builder < Trailblazer::Activity::Railway
|
5
|
+
# step :build_policy
|
6
|
+
step :build_protocol_block
|
7
|
+
step :normalize_tuple
|
8
|
+
|
9
|
+
# def build_policy(ctx, policies:, **)
|
10
|
+
# end
|
11
|
+
|
12
|
+
def build_protocol_block(ctx, policy:, **)
|
13
|
+
ctx[:protocol_block] = -> { step Subprocess(policy), id: :policy, replace: :policy, inherit: true; {} }
|
14
|
+
end
|
15
|
+
|
16
|
+
def normalize_tuple(ctx, protocol_block:, options_for_build: {}, **)
|
17
|
+
ctx[:build_options] = {
|
18
|
+
protocol_block: protocol_block,
|
19
|
+
options_for_build: options_for_build
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
module DSL
|
25
|
+
module_function
|
26
|
+
#
|
27
|
+
# @return endpoint_options
|
28
|
+
|
29
|
+
def build_options_for(builder:, **options)
|
30
|
+
signal, (ctx, _) = builder.([options])
|
31
|
+
|
32
|
+
ctx[:build_options] # ["web:submitted?", {protocol_block: ..., options_for_build: ...}]
|
33
|
+
end
|
34
|
+
|
35
|
+
def endpoint_for(id:, builder:, default_options:, **config)
|
36
|
+
options = build_options_for(builder: builder, **config)
|
37
|
+
|
38
|
+
return id, Trailblazer::Endpoint.build(default_options.merge(options[:options_for_build]), &options[:protocol_block])
|
39
|
+
end
|
40
|
+
|
41
|
+
# {dsl_options} being something like
|
42
|
+
#
|
43
|
+
# "api:Start.default" => {policies: []},
|
44
|
+
# "api:status?" => {policies: [:user_owns_diagram]},
|
45
|
+
# "api:download?" => {policies: [:user_owns_diagram]},
|
46
|
+
# "api:delete?" => {policies: [:user_owns_diagram]},
|
47
|
+
def endpoints_for(dsl_options, **options)
|
48
|
+
endpoints = dsl_options.collect do |id, config|
|
49
|
+
endpoint_for(id: id, **options, **config) # config is per endpoint, options are "global"
|
50
|
+
end.to_h
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end # Builder
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
class Endpoint
|
3
|
+
# The {Protocol} implements auth*, and calls the domain OP/WF.
|
4
|
+
# You still have to implement handlers (like {#authorize} and {#handle_not_authorized}) yourself. This might change soon.
|
5
|
+
#
|
6
|
+
# Protocol must provide all ends for the Adapter (401,403 and 404 in particular), even if the ran op/workflow doesn't have it.
|
7
|
+
# Still thinking about how to do that best.
|
8
|
+
|
9
|
+
# Termini and their "pendants" in HTTP, which is unrelated to protocol!! Protocol is application-focused and doesn't know about HTTP.
|
10
|
+
# failure: 411
|
11
|
+
# success: 200
|
12
|
+
# not_found: 404
|
13
|
+
# not_authenticated: 401
|
14
|
+
# not_authorized: 403
|
15
|
+
class Protocol < Trailblazer::Activity::Railway
|
16
|
+
class Noop < Trailblazer::Activity::Railway
|
17
|
+
end
|
18
|
+
|
19
|
+
class Failure < Trailblazer::Activity::End # DISCUSS: move to Act::Railway?
|
20
|
+
# class Authentication < Failure
|
21
|
+
# end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self._Path(semantic:, &block) # DISCUSS: the problem with Path currently is https://github.com/trailblazer/trailblazer-activity-dsl-linear/issues/27
|
25
|
+
Path(track_color: semantic, end_id: "End.#{semantic}", end_task: Failure.new(semantic: semantic), &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
step :authenticate, Output(:failure) => _Path(semantic: :not_authenticated) do
|
29
|
+
# step :handle_not_authenticated
|
30
|
+
end
|
31
|
+
|
32
|
+
step :policy, Output(:failure) => _Path(semantic: :not_authorized) do # user from cookie, etc
|
33
|
+
# step :handle_not_authorized
|
34
|
+
end
|
35
|
+
|
36
|
+
# Here, we test a domain OP with ADDITIONAL explicit ends that get wired to the Adapter (vaidation_error => failure).
|
37
|
+
# We still need to test the other way round: wiring a "normal" failure to, say, not_found, by inspecting the ctx.
|
38
|
+
step Subprocess(Noop), id: :domain_activity
|
39
|
+
|
40
|
+
|
41
|
+
|
42
|
+
# add the {End.not_found} terminus to this Protocol. I'm not sure that's the final style, but since a {Protocol} needs to provide all
|
43
|
+
# termini for the Adapter this is the only way to get it working right now.
|
44
|
+
# FIXME: is this really the only way to add an {End} to all this?
|
45
|
+
@state.update_sequence do |sequence:, **|
|
46
|
+
sequence = Activity::Path::DSL.append_end(sequence, task: Failure.new(semantic: :not_found), magnetic_to: :not_found, id: "End.not_found")
|
47
|
+
sequence = Activity::Path::DSL.append_end(sequence, task: Failure.new(semantic: :invalid_data), magnetic_to: :invalid_data, id: "End.invalid_data")
|
48
|
+
|
49
|
+
recompile_activity!(sequence)
|
50
|
+
|
51
|
+
sequence
|
52
|
+
end
|
53
|
+
|
54
|
+
# Best-practices of useful routes and handlers that work with 2.1-OPs.
|
55
|
+
class Standard < Protocol
|
56
|
+
step :handle_not_authenticated, magnetic_to: :not_authenticated, Output(:success) => Track(:not_authenticated), Output(:failure) => Track(:not_authenticated)#, before: "End.not_authenticated"
|
57
|
+
step :handle_not_authorized, magnetic_to: :not_authorized, Output(:success) => Track(:not_authorized), Output(:failure) => Track(:not_authorized)
|
58
|
+
# step :handle_invalid_data, magnetic_to: :invalid_data, Output(:success) => Track(:invalid_data), Output(:failure) => Track(:invalid_data)
|
59
|
+
|
60
|
+
|
61
|
+
# TODO: allow translation.
|
62
|
+
module Handler
|
63
|
+
def handle_not_authorized(ctx, errors:, **)
|
64
|
+
errors.message = "Action not allowed due to a policy setting."
|
65
|
+
end
|
66
|
+
|
67
|
+
def handle_not_authenticated(ctx, errors:, **)
|
68
|
+
errors.message = "Authentication credentials were not provided or are invalid."
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class Termini # FIXME: this means with invalid_data, not_found termini? 2.1?
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
module Bridge
|
78
|
+
# this "bridge" should be optional for "legacy operations" that don't have explicit ends.
|
79
|
+
# we have to inspect the ctx to find out what "really" happened (e.g. model empty ==> 404)
|
80
|
+
NotFound = Class.new(Trailblazer::Activity::Signal)
|
81
|
+
NotAuthorized = Class.new(Trailblazer::Activity::Signal)
|
82
|
+
NotAuthenticated = Class.new(Trailblazer::Activity::Signal)
|
83
|
+
|
84
|
+
def self.insert(protocol, **)
|
85
|
+
Class.new(protocol) do
|
86
|
+
fail :success?, after: :domain_activity,
|
87
|
+
# FIXME: how to add more signals/outcomes?
|
88
|
+
Output(NotFound, :not_found) => Track(:not_found),
|
89
|
+
|
90
|
+
# FIXME: Track(:not_authorized) is defined before this step, so the Forward search doesn't find it.
|
91
|
+
# solution would be to walk down sequence and find the first {:magnetic_to} "not_authorized"
|
92
|
+
Output(NotAuthorized, :not_authorized) => Track(:not_authorized) # FIXME: how to "insert into path"? => Track(:not_authorized) doesn't play!
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
module Domain
|
99
|
+
# taskWrap step that saves the return signal of the {domain_activity}.
|
100
|
+
# The taskWrap step is usually inserted after {task_wrap.output}.
|
101
|
+
def self.terminus_handler(wrap_ctx, original_args)
|
102
|
+
|
103
|
+
# Unrecognized Signal `"bla"` returned from EndpointTest::LegacyCreate. Registered signals are,
|
104
|
+
# - #<Trailblazer::Activity::End semantic=:failure>
|
105
|
+
# - #<Trailblazer::Activity::End semantic=:success>
|
106
|
+
# - #<Trailblazer::Activity::End semantic=:fromail_fast>
|
107
|
+
|
108
|
+
# {:return_args} is the original "endpoint ctx" that was returned from the {:output} filter.
|
109
|
+
wrap_ctx[:return_args][0][:domain_activity_return_signal] = wrap_ctx[:return_signal]
|
110
|
+
|
111
|
+
return wrap_ctx, original_args
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.extension_for_terminus_handler
|
115
|
+
# this is called after {:output}.
|
116
|
+
[[Trailblazer::Activity::TaskWrap::Pipeline.method(:insert_after), "task_wrap.call_task", ["endpoint.end_signal", method(:terminus_handler)]]]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|