trailblazer 2.1.0.beta1 → 2.1.0.beta2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|