trailblazer-endpoint 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGES.md +3 -0
- data/Gemfile +16 -0
- data/README.md +11 -0
- data/Rakefile +10 -0
- data/lib/trailblazer-endpoint.rb +1 -0
- data/lib/trailblazer/endpoint.rb +133 -0
- data/lib/trailblazer/endpoint/adapter.rb +197 -0
- data/lib/trailblazer/endpoint/builder.rb +56 -0
- data/lib/trailblazer/endpoint/protocol.rb +122 -0
- data/lib/trailblazer/endpoint/rails.rb +27 -0
- data/lib/trailblazer/endpoint/version.rb +5 -0
- data/test/adapter/api_test.rb +78 -0
- data/test/adapter/representable_test.rb +7 -0
- data/test/adapter/web_test.rb +40 -0
- data/test/benchmark/skill_resolver_benchmark.rb +43 -0
- data/test/docs/controller_test.rb +92 -0
- data/test/docs/endpoint_test.rb +54 -0
- data/test/endpoint_test.rb +908 -0
- data/test/rails-app/.gitignore +21 -0
- data/test/rails-app/Gemfile +17 -0
- data/test/rails-app/Gemfile.lock +157 -0
- data/test/rails-app/README.md +24 -0
- data/test/rails-app/Rakefile +6 -0
- data/test/rails-app/app/controllers/application_controller.rb +3 -0
- data/test/rails-app/app/controllers/songs_controller.rb +26 -0
- data/test/rails-app/app/models/application_record.rb +3 -0
- data/test/rails-app/app/models/concerns/.keep +0 -0
- data/test/rails-app/config.ru +5 -0
- data/test/rails-app/config/application.rb +15 -0
- data/test/rails-app/config/boot.rb +3 -0
- data/test/rails-app/config/database.yml +25 -0
- data/test/rails-app/config/environment.rb +5 -0
- data/test/rails-app/config/environments/development.rb +54 -0
- data/test/rails-app/config/environments/production.rb +86 -0
- data/test/rails-app/config/environments/test.rb +42 -0
- data/test/rails-app/config/initializers/application_controller_renderer.rb +6 -0
- data/test/rails-app/config/initializers/backtrace_silencers.rb +7 -0
- data/test/rails-app/config/initializers/cookies_serializer.rb +5 -0
- data/test/rails-app/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/rails-app/config/initializers/inflections.rb +16 -0
- data/test/rails-app/config/initializers/mime_types.rb +4 -0
- data/test/rails-app/config/initializers/new_framework_defaults.rb +24 -0
- data/test/rails-app/config/initializers/session_store.rb +3 -0
- data/test/rails-app/config/initializers/wrap_parameters.rb +14 -0
- data/test/rails-app/config/locales/en.yml +23 -0
- data/test/rails-app/config/routes.rb +6 -0
- data/test/rails-app/config/secrets.yml +22 -0
- data/test/rails-app/db/seeds.rb +7 -0
- data/test/rails-app/log/.keep +0 -0
- data/test/rails-app/test/controllers/.keep +0 -0
- data/test/rails-app/test/controllers/songs_controller_test.rb +156 -0
- data/test/rails-app/test/fixtures/.keep +0 -0
- data/test/rails-app/test/fixtures/files/.keep +0 -0
- data/test/rails-app/test/helpers/.keep +0 -0
- data/test/rails-app/test/integration/.keep +0 -0
- data/test/rails-app/test/mailers/.keep +0 -0
- data/test/rails-app/test/models/.keep +0 -0
- data/test/rails-app/test/test_helper.rb +10 -0
- data/test/rails-app/tmp/.keep +0 -0
- data/test/rails-app/vendor/assets/javascripts/.keep +0 -0
- data/test/rails-app/vendor/assets/stylesheets/.keep +0 -0
- data/test/test_helper.rb +34 -0
- data/trailblazer-endpoint.gemspec +26 -0
- metadata +236 -0
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
|