trailblazer 2.1.0.beta1 → 2.1.0.beta2
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 +4 -4
- data/.rubocop.yml +18 -0
- data/.rubocop_todo.yml +825 -0
- data/CHANGES.md +4 -0
- data/Gemfile +5 -2
- data/Rakefile +3 -0
- data/lib/trailblazer.rb +1 -0
- data/lib/trailblazer/operation/contract.rb +57 -52
- data/lib/trailblazer/operation/input_output.rb +28 -0
- data/lib/trailblazer/operation/model.rb +5 -5
- data/lib/trailblazer/operation/nested.rb +40 -34
- data/lib/trailblazer/operation/persist.rb +9 -7
- data/lib/trailblazer/operation/policy.rb +4 -4
- data/lib/trailblazer/operation/validate.rb +57 -58
- data/lib/trailblazer/operation/wrap.rb +4 -5
- data/lib/trailblazer/task.rb +19 -19
- data/lib/trailblazer/version.rb +1 -1
- data/test/docs/nested_test.rb +49 -15
- data/test/test_helper.rb +1 -0
- data/trailblazer.gemspec +1 -1
- metadata +9 -6
data/CHANGES.md
CHANGED
@@ -29,6 +29,10 @@ document Task API and define step API
|
|
29
29
|
deprecate step->(options) ?
|
30
30
|
injectable, per-operation step arguments strategy?
|
31
31
|
|
32
|
+
# 2.1.0.beta2
|
33
|
+
|
34
|
+
* Simplify `Nested` and several other internals by using the new `Activity` API.
|
35
|
+
|
32
36
|
# 2.1.0.beta1
|
33
37
|
|
34
38
|
* Add `deprecation/call` and `deprecation/context` that help with the new `call` API and symbols for `options` keys.
|
data/Gemfile
CHANGED
@@ -14,12 +14,15 @@ gem "dry-auto_inject"
|
|
14
14
|
gem "dry-matcher"
|
15
15
|
gem "dry-validation"
|
16
16
|
|
17
|
-
gem "trailblazer-operation", path: "../operation"
|
17
|
+
# gem "trailblazer-operation", path: "../operation"
|
18
18
|
# gem "trailblazer-operation", github: "trailblazer/trailblazer-operation"
|
19
19
|
|
20
20
|
gem "minitest-line"
|
21
21
|
|
22
|
-
gem "
|
22
|
+
gem "rubocop", require: false
|
23
|
+
# gem "trailblazer-activity", path: "../trailblazer-circuit"
|
24
|
+
# gem "trailblazer-activity", github: "trailblazer/trailblazer-activity"
|
25
|
+
# gem "trailblazer-activity", path: "../trailblazer-circuit"
|
23
26
|
# gem "trailblazer-activity", github: "trailblazer/trailblazer-activity"
|
24
27
|
|
25
28
|
# gem "trailblazer-context", path: "../trailblazer-context"
|
data/Rakefile
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rake/testtask"
|
3
|
+
require "rubocop/rake_task"
|
3
4
|
|
4
5
|
task :default => [:test]
|
5
6
|
|
@@ -14,3 +15,5 @@ Rake::TestTask.new(:testdep) do |test|
|
|
14
15
|
test.test_files = FileList["test/deprecation/*_test.rb"]
|
15
16
|
test.verbose = true
|
16
17
|
end
|
18
|
+
|
19
|
+
RuboCop::RakeTask.new
|
data/lib/trailblazer.rb
CHANGED
@@ -6,72 +6,77 @@
|
|
6
6
|
#
|
7
7
|
# Needs Operation#model.
|
8
8
|
# Needs #[], #[]= skill dependency.
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
def self.call(options, circuit_options, name: "default", constant: nil, builder: nil)
|
22
|
-
# TODO: we could probably clean this up a bit at some point.
|
23
|
-
contract_class = constant || options["contract.#{name}.class"] # DISCUSS: Injection possible here?
|
24
|
-
model = options[:model]
|
25
|
-
name = "contract.#{name}"
|
26
|
-
|
27
|
-
options[name] =
|
28
|
-
if builder
|
29
|
-
call_builder( options, circuit_options, builder: builder, constant: contract_class, name: name )
|
30
|
-
else
|
31
|
-
contract_class.new(model)
|
32
|
-
end
|
9
|
+
module Trailblazer
|
10
|
+
class Operation
|
11
|
+
module Contract
|
12
|
+
def self.Build(name: "default", constant: nil, builder: nil)
|
13
|
+
task = ->((options, flow_options), **circuit_options) do
|
14
|
+
result = Build.(options, circuit_options, name: name, constant: constant, builder: builder)
|
15
|
+
|
16
|
+
return Activity::TaskBuilder::Binary.binary_direction_for( result, Activity::Right, Activity::Left ),
|
17
|
+
[options, flow_options]
|
18
|
+
end
|
19
|
+
|
20
|
+
{ task: task, id: "contract.build" }
|
33
21
|
end
|
34
22
|
|
35
|
-
|
36
|
-
#
|
37
|
-
|
38
|
-
|
23
|
+
module Build
|
24
|
+
# Build contract at runtime.
|
25
|
+
def self.call(options, circuit_options, name: "default", constant: nil, builder: nil)
|
26
|
+
# TODO: we could probably clean this up a bit at some point.
|
27
|
+
contract_class = constant || options["contract.#{name}.class"] # DISCUSS: Injection possible here?
|
28
|
+
model = options[:model]
|
29
|
+
name = "contract.#{name}"
|
39
30
|
|
31
|
+
options[name] =
|
32
|
+
if builder
|
33
|
+
call_builder( options, circuit_options, builder: builder, constant: contract_class, name: name )
|
34
|
+
else
|
35
|
+
contract_class.new(model)
|
36
|
+
end
|
37
|
+
end
|
40
38
|
|
39
|
+
def self.call_builder(options, circuit_options, builder:raise, constant:raise, name:raise)
|
40
|
+
# builder_options = Trailblazer::Context( options, constant: constant, name: name ) # options.merge( .. )
|
41
41
|
|
42
|
+
# Trailblazer::Option::KW(builder).(builder_options, circuit_options)
|
42
43
|
|
43
44
|
|
44
45
|
|
45
46
|
|
46
47
|
|
47
|
-
# FIXME: almost identical with Option::KW.
|
48
|
-
# FIXME: see Nested::Options::Dynamic, the same shit
|
49
|
-
tmp_options = options.to_hash.merge(
|
50
|
-
constant: constant,
|
51
|
-
name: name
|
52
|
-
)
|
53
48
|
|
54
|
-
Trailblazer::Option(builder).( options, tmp_options, circuit_options )
|
55
|
-
end
|
56
|
-
end
|
57
49
|
|
58
|
-
module DSL
|
59
|
-
def self.extended(extender)
|
60
|
-
extender.extend(ClassDependencies)
|
61
|
-
warn "[Trailblazer] Using `contract do...end` is deprecated. Please use a form class and the Builder( constant: <Form> ) option."
|
62
|
-
end
|
63
|
-
# This is the class level DSL method.
|
64
|
-
# Op.contract #=> returns contract class
|
65
|
-
# Op.contract do .. end # defines contract
|
66
|
-
# Op.contract CommentForm # copies (and subclasses) external contract.
|
67
|
-
# Op.contract CommentForm do .. end # copies and extends contract.
|
68
|
-
def contract(name=:default, constant=nil, base: Reform::Form, &block)
|
69
|
-
heritage.record(:contract, name, constant, &block)
|
70
50
|
|
71
|
-
|
51
|
+
# FIXME: almost identical with Option::KW.
|
52
|
+
# FIXME: see Nested::Options::Dynamic, the same shit
|
53
|
+
tmp_options = options.to_hash.merge(
|
54
|
+
constant: constant,
|
55
|
+
name: name
|
56
|
+
)
|
72
57
|
|
73
|
-
|
58
|
+
Trailblazer::Option(builder).( options, tmp_options, circuit_options )
|
59
|
+
end
|
74
60
|
end
|
75
|
-
|
61
|
+
|
62
|
+
module DSL
|
63
|
+
def self.extended(extender)
|
64
|
+
extender.extend(ClassDependencies)
|
65
|
+
warn "[Trailblazer] Using `contract do...end` is deprecated. Please use a form class and the Builder( constant: <Form> ) option."
|
66
|
+
end
|
67
|
+
# This is the class level DSL method.
|
68
|
+
# Op.contract #=> returns contract class
|
69
|
+
# Op.contract do .. end # defines contract
|
70
|
+
# Op.contract CommentForm # copies (and subclasses) external contract.
|
71
|
+
# Op.contract CommentForm do .. end # copies and extends contract.
|
72
|
+
def contract(name=:default, constant=nil, base: Reform::Form, &block)
|
73
|
+
heritage.record(:contract, name, constant, &block)
|
74
|
+
|
75
|
+
path, form_class = Trailblazer::DSL::Build.new.({ prefix: :contract, class: base, container: self }, name, constant, block)
|
76
|
+
|
77
|
+
self[path] = form_class
|
78
|
+
end
|
79
|
+
end # Contract
|
80
|
+
end
|
76
81
|
end
|
77
82
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
# Add an input and output filter for a task, allowing to control what a task "sees"
|
3
|
+
# (receives as input) and returns (or, what the outer caller "sees").
|
4
|
+
#
|
5
|
+
# This works by adding two variable mappers to the taskWrap.
|
6
|
+
# One before the actual task gets called (input) and one before the end (output).
|
7
|
+
module Operation::InputOutput
|
8
|
+
# naming: Macaroni, VariableMapping
|
9
|
+
def self.plan(input, output)
|
10
|
+
default_input_filter = ->(options, *) { ctx = options }
|
11
|
+
default_output_filter = ->(options, *) { options }
|
12
|
+
|
13
|
+
input ||= default_input_filter
|
14
|
+
output ||= default_output_filter
|
15
|
+
|
16
|
+
input_filter = Activity::TaskWrap::Input.new(input)
|
17
|
+
output_filter = Activity::TaskWrap::Output.new(output)
|
18
|
+
|
19
|
+
# taskWrap extensions
|
20
|
+
Module.new do
|
21
|
+
extend Activity::Path::Plan()
|
22
|
+
|
23
|
+
task input_filter, id: ".input", before: "task_wrap.call_task"
|
24
|
+
task output_filter, id: ".output", before: "End.success", group: :end # DISCUSS: position
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -2,16 +2,16 @@ class Trailblazer::Operation
|
|
2
2
|
def self.Model(model_class, action=nil)
|
3
3
|
# step = Pipetree::Step.new(step, "model.class" => model_class, "model.action" => action)
|
4
4
|
|
5
|
-
task =
|
5
|
+
task = Trailblazer::Activity::TaskBuilder::Binary.( Model.new )
|
6
6
|
|
7
|
-
|
8
|
-
|
7
|
+
extension = Trailblazer::Activity::TaskWrap::Merge.new(
|
8
|
+
Wrap::Inject::Defaults(
|
9
9
|
"model.class" => model_class,
|
10
10
|
"model.action" => action
|
11
11
|
)
|
12
|
-
|
12
|
+
)
|
13
13
|
|
14
|
-
{ task: task, id: "model.build",
|
14
|
+
{ task: task, id: "model.build", extension: [extension] }
|
15
15
|
end
|
16
16
|
|
17
17
|
class Model
|
@@ -2,43 +2,40 @@
|
|
2
2
|
module Trailblazer
|
3
3
|
class Operation
|
4
4
|
def self.Nested(callable, input:nil, output:nil, id: "Nested(#{callable})")
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
# @needs operation#outputs
|
9
|
-
|
10
|
-
# TODO: move this to the generic step DSL
|
11
|
-
task_wrap_extensions = []
|
12
|
-
|
13
|
-
if input || output
|
14
|
-
default_input_filter = ->(options, *) { ctx = options }
|
15
|
-
default_output_filter = ->(options, *) { options }
|
5
|
+
task_wrap_extensions = Module.new do
|
6
|
+
extend Activity::Path::Plan()
|
7
|
+
end
|
16
8
|
|
17
|
-
|
18
|
-
output ||= default_output_filter
|
9
|
+
task, operation, is_dynamic = Nested.build(callable)
|
19
10
|
|
20
|
-
|
21
|
-
|
11
|
+
if input || output # TODO: move this to the generic step DSL
|
12
|
+
task_wrap_extensions = InputOutput.plan( input, output )
|
13
|
+
end
|
22
14
|
|
23
|
-
|
24
|
-
|
25
|
-
task
|
26
|
-
|
15
|
+
if is_dynamic
|
16
|
+
# task_wrap_extensions += Activity::Magnetic::Builder::Path.plan do
|
17
|
+
task_wrap_extensions.task task.method(:compute_nested_activity), id: ".compute_nested_activity", after: "Start.default", group: :start
|
18
|
+
task_wrap_extensions.task task.method(:compute_return_signal), id: ".compute_return_signal", after: "task_wrap.call_task"
|
19
|
+
# end
|
27
20
|
end
|
28
|
-
# Default {Output} copies the mutable data from the nested activity into the original.
|
29
21
|
|
30
|
-
{
|
22
|
+
{
|
23
|
+
task: task,
|
24
|
+
id: id,
|
25
|
+
extension: [ Trailblazer::Activity::TaskWrap::Merge.new(task_wrap_extensions) ],
|
26
|
+
plus_poles: Activity::Magnetic::DSL::PlusPoles.from_outputs(operation.outputs) # @needs operation#outputs
|
27
|
+
}
|
31
28
|
end
|
32
29
|
|
33
30
|
# @private
|
34
31
|
module Nested
|
35
|
-
def self.build(nested_operation
|
36
|
-
return dynamic = Dynamic.new(nested_operation), dynamic unless nestable_object?(nested_operation)
|
32
|
+
def self.build(nested_operation) # DISCUSS: use builders here?
|
33
|
+
return dynamic = Dynamic.new(nested_operation), dynamic, true unless nestable_object?(nested_operation)
|
37
34
|
|
38
35
|
# The returned {Nested} instance is a valid circuit element and will be `call`ed in the circuit.
|
39
36
|
# It simply returns the nested activity's `signal,options,flow_options` return set.
|
40
37
|
# The actual wiring - where to go with that - is done by the step DSL.
|
41
|
-
return Trailblazer::Activity::Subprocess(nested_operation, call: :__call__), nested_operation
|
38
|
+
return Trailblazer::Activity::Subprocess(nested_operation, call: :__call__), nested_operation, false
|
42
39
|
end
|
43
40
|
|
44
41
|
def self.nestable_object?(object)
|
@@ -56,9 +53,9 @@ module Trailblazer
|
|
56
53
|
#
|
57
54
|
# This is what {Nested} in 2.0 used to do, where the outcome could only be true/false (or success/failure).
|
58
55
|
class Dynamic
|
59
|
-
def initialize(
|
60
|
-
@
|
61
|
-
@outputs
|
56
|
+
def initialize(nested_activity)
|
57
|
+
@nested_activity = Trailblazer::Option::KW(nested_activity)
|
58
|
+
@outputs = {
|
62
59
|
:success => Activity::Output( Railway::End::Success.new(:success), :success ),
|
63
60
|
:failure => Activity::Output( Railway::End::Failure.new(:failure), :failure ),
|
64
61
|
}
|
@@ -66,17 +63,26 @@ module Trailblazer
|
|
66
63
|
|
67
64
|
attr_reader :outputs
|
68
65
|
|
69
|
-
|
70
|
-
|
66
|
+
# TaskWrap step.
|
67
|
+
def compute_nested_activity( (wrap_ctx, original_args), **circuit_options )
|
68
|
+
(ctx, _), original_circuit_options = original_args
|
69
|
+
|
70
|
+
activity = @nested_activity.( ctx, original_circuit_options ) # evaluate the option to get the actual "object" to call.
|
71
71
|
|
72
|
-
|
72
|
+
# overwrite :task so task_wrap.call_task will call this activity. This is a trick so we don't have to repeat
|
73
|
+
# logic from #call_task here.
|
74
|
+
wrap_ctx[:task] = Trailblazer::Activity::Subprocess( activity, call: :__call__ )
|
73
75
|
|
76
|
+
return Activity::Right, [ wrap_ctx, original_args ]
|
77
|
+
end
|
78
|
+
|
79
|
+
def compute_return_signal( (wrap_ctx, original_args), **circuit_options )
|
74
80
|
# Translate the genuine nested signal to the generic Dynamic end (success/failure, only).
|
75
81
|
# Note that here we lose information about what specific event was emitted.
|
76
|
-
[
|
77
|
-
|
78
|
-
|
79
|
-
]
|
82
|
+
wrap_ctx[:return_signal] = wrap_ctx[:return_signal].kind_of?(Railway::End::Success) ?
|
83
|
+
@outputs[:success].signal : @outputs[:failure].signal
|
84
|
+
|
85
|
+
return Activity::Right, [ wrap_ctx, original_args ]
|
80
86
|
end
|
81
87
|
end
|
82
88
|
end
|
@@ -1,12 +1,14 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
module Trailblazer
|
2
|
+
class Operation
|
3
|
+
module Contract
|
4
|
+
def self.Persist(method: :save, name: "default")
|
5
|
+
path = "contract.#{name}"
|
6
|
+
step = ->(options, **) { options[path].send(method) }
|
6
7
|
|
7
|
-
|
8
|
+
task = Activity::TaskBuilder::Binary.( step )
|
8
9
|
|
9
|
-
|
10
|
+
{ task: task, id: "persist.save" }
|
11
|
+
end
|
10
12
|
end
|
11
13
|
end
|
12
14
|
end
|
@@ -32,13 +32,13 @@ class Trailblazer::Operation
|
|
32
32
|
|
33
33
|
task = Eval.new( name: name, path: path )
|
34
34
|
|
35
|
-
|
36
|
-
|
35
|
+
extension = Trailblazer::Activity::TaskWrap::Merge.new(
|
36
|
+
Trailblazer::Operation::Wrap::Inject::Defaults(
|
37
37
|
path => condition
|
38
38
|
)
|
39
|
-
|
39
|
+
)
|
40
40
|
|
41
|
-
{ task: task, id: path,
|
41
|
+
{ task: task, id: path, extension: [extension] }
|
42
42
|
end
|
43
43
|
end
|
44
44
|
end
|
@@ -1,75 +1,74 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
1
|
+
module Trailblazer
|
2
|
+
class Operation
|
3
|
+
module Contract
|
4
|
+
# result.contract = {..}
|
5
|
+
# result.contract.errors = {..}
|
6
|
+
# Deviate to left track if optional key is not found in params.
|
7
|
+
# Deviate to left if validation result falsey.
|
8
|
+
def self.Validate(skip_extract: false, name: "default", representer: false, key: nil) # DISCUSS: should we introduce something like Validate::Deserializer?
|
9
|
+
params_path = "contract.#{name}.params" # extract_params! save extracted params here.
|
9
10
|
|
10
|
-
|
11
|
-
|
11
|
+
extract = Validate::Extract.new( key: key, params_path: params_path ).freeze
|
12
|
+
validate = Validate.new( name: name, representer: representer, params_path: params_path ).freeze
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
# Build a simple Railway {Activity} for the internal flow.
|
15
|
+
activity = Module.new do
|
16
|
+
extend Activity::Railway()
|
17
|
+
|
18
|
+
step extract, id: "#{params_path}_extract" unless skip_extract || representer
|
19
|
+
step validate, id: "contract.#{name}.call"
|
20
|
+
end
|
17
21
|
|
22
|
+
# activity, _ = activity.decompose
|
18
23
|
|
19
|
-
|
20
|
-
|
21
|
-
step Trailblazer::Activity::Task::Binary( extract ), id: "#{params_path}_extract"
|
22
|
-
step Trailblazer::Activity::Task::Binary( validate ), id: "contract.#{name}.call"
|
24
|
+
# DISCUSS: use Nested here?
|
25
|
+
{ task: activity, id: "contract.#{name}.validate", plus_poles: Activity::Magnetic::DSL::PlusPoles.from_outputs(activity.outputs) }
|
23
26
|
end
|
24
27
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
28
|
+
class Validate
|
29
|
+
# Task: extract the contract's input from params by reading `:key`.
|
30
|
+
class Extract
|
31
|
+
def initialize(key:nil, params_path:nil)
|
32
|
+
@key, @params_path = key, params_path
|
33
|
+
end
|
29
34
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
def initialize(key:nil, params_path:nil)
|
34
|
-
@key, @params_path = key, params_path
|
35
|
+
def call( ctx, params:, ** )
|
36
|
+
ctx[@params_path] = @key ? params[@key] : params
|
37
|
+
end
|
35
38
|
end
|
36
39
|
|
37
|
-
def
|
38
|
-
|
40
|
+
def initialize(name:"default", representer:false, params_path:nil)
|
41
|
+
@name, @representer, @params_path = name, representer, params_path
|
39
42
|
end
|
40
|
-
end
|
41
43
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
representer: options["representer.#{@name}.class"] ||= @representer, # FIXME: maybe @representer should use DI.
|
51
|
-
params_path: @params_path
|
52
|
-
)
|
53
|
-
end
|
44
|
+
# Task: Validates contract `:name`.
|
45
|
+
def call( ctx, ** )
|
46
|
+
validate!(
|
47
|
+
ctx,
|
48
|
+
representer: ctx["representer.#{@name}.class"] ||= @representer, # FIXME: maybe @representer should use DI.
|
49
|
+
params_path: @params_path
|
50
|
+
)
|
51
|
+
end
|
54
52
|
|
55
|
-
|
56
|
-
|
57
|
-
|
53
|
+
def validate!(options, representer:false, from: :document, params_path:nil)
|
54
|
+
path = "contract.#{@name}"
|
55
|
+
contract = options[path]
|
58
56
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
57
|
+
# this is for 1.1-style compatibility and should be removed once we have Deserializer in place:
|
58
|
+
options["result.#{path}"] = result =
|
59
|
+
if representer
|
60
|
+
# use :document as the body and let the representer deserialize to the contract.
|
61
|
+
# this will be simplified once we have Deserializer.
|
62
|
+
# translates to contract.("{document: bla}") { MyRepresenter.new(contract).from_json .. }
|
63
|
+
contract.(options[from]) { |document| representer.new(contract).parse(document) }
|
64
|
+
else
|
65
|
+
# let Reform handle the deserialization.
|
66
|
+
contract.(options[params_path])
|
67
|
+
end
|
70
68
|
|
71
|
-
|
69
|
+
result.success?
|
70
|
+
end
|
72
71
|
end
|
73
72
|
end
|
74
|
-
end
|
73
|
+
end # Operation
|
75
74
|
end
|