trailblazer-macro 2.1.0.rc1 → 2.1.0.rc11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.rubocop.yml +4 -14
- data/.rubocop_todo.yml +109 -241
- data/.travis.yml +10 -14
- data/CHANGES.md +5 -0
- data/Gemfile +3 -1
- data/Rakefile +1 -8
- data/lib/trailblazer-macro.rb +1 -1
- data/lib/trailblazer/macro.rb +27 -9
- data/lib/trailblazer/{operation → macro}/guard.rb +4 -4
- data/lib/trailblazer/{operation → macro}/model.rb +8 -10
- data/lib/trailblazer/macro/nested.rb +76 -0
- data/lib/trailblazer/{operation → macro}/policy.rb +9 -11
- data/lib/trailblazer/{operation → macro}/pundit.rb +5 -5
- data/lib/trailblazer/macro/rescue.rb +44 -0
- data/lib/trailblazer/macro/version.rb +4 -2
- data/lib/trailblazer/macro/wrap.rb +80 -0
- data/test/docs/guard_test.rb +4 -1
- data/test/docs/macro_test.rb +2 -2
- data/test/docs/nested_test.rb +78 -82
- data/test/docs/rescue_test.rb +10 -7
- data/test/docs/wrap_test.rb +14 -6
- data/test/operation/integration_test.rb +55 -0
- data/test/operation/model_test.rb +15 -5
- data/test/operation/pundit_test.rb +0 -1
- data/test/test_helper.rb +14 -4
- data/trailblazer-macro.gemspec +6 -3
- metadata +40 -28
- data/lib/trailblazer/operation/nested.rb +0 -98
- data/lib/trailblazer/operation/rescue.rb +0 -42
- data/lib/trailblazer/operation/wrap.rb +0 -83
- data/test/lib/methods.rb +0 -25
- data/test/operation/nested_test.rb +0 -293
data/.travis.yml
CHANGED
@@ -1,15 +1,11 @@
|
|
1
|
+
sudo: false
|
1
2
|
language: ruby
|
2
|
-
|
3
|
-
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
gemfile: Gemfile
|
12
|
-
- rvm: 2.4.1
|
13
|
-
gemfile: Gemfile
|
14
|
-
- rvm: 2.5.0
|
15
|
-
gemfile: Gemfile
|
3
|
+
rvm:
|
4
|
+
- 2.6.2
|
5
|
+
- 2.5.5
|
6
|
+
- 2.4.4
|
7
|
+
- 2.3.7
|
8
|
+
- 2.2
|
9
|
+
- 2.1
|
10
|
+
|
11
|
+
cache: bundler
|
data/CHANGES.md
CHANGED
data/Gemfile
CHANGED
@@ -5,7 +5,9 @@ gemspec
|
|
5
5
|
|
6
6
|
# gem "trailblazer", github: "trailblazer/trailblazer"
|
7
7
|
# gem "trailblazer-activity", path: "../trailblazer-activity"
|
8
|
-
# gem "trailblazer-
|
8
|
+
# gem "trailblazer-activity-dsl-linear", github: "trailblazer/trailblazer-activity-dsl-linear", branch: 'master'
|
9
|
+
# gem "trailblazer-activity-dsl-linear", path: "../trailblazer-activity-dsl-linear"
|
10
|
+
# gem "trailblazer-operation", github: "trailblazer/trailblazer-operation", branch: 'linear'
|
9
11
|
# gem "trailblazer-activity"#, github: "trailblazer/trailblazer-activity"
|
10
12
|
# gem "trailblazer-macro-contract", git: "https://github.com/trailblazer/trailblazer-macro-contract"
|
11
13
|
|
data/Rakefile
CHANGED
@@ -1,8 +1,5 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rake/testtask"
|
3
|
-
require "rubocop/rake_task"
|
4
|
-
|
5
|
-
task :default => %i[test rubocop]
|
6
3
|
|
7
4
|
Rake::TestTask.new(:test) do |test|
|
8
5
|
test.libs << 'test'
|
@@ -10,8 +7,4 @@ Rake::TestTask.new(:test) do |test|
|
|
10
7
|
test.verbose = true
|
11
8
|
end
|
12
9
|
|
13
|
-
|
14
|
-
task.patterns = ['lib/**/*.rb', 'test/**/*.rb']
|
15
|
-
task.options << "--display-cop-names"
|
16
|
-
task.fail_on_error = false
|
17
|
-
end
|
10
|
+
task default: :test
|
data/lib/trailblazer-macro.rb
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
require "trailblazer/macro/version"
|
1
|
+
require "trailblazer/macro/version" # rubocop:disable Naming/FileName
|
2
2
|
require "trailblazer/macro"
|
data/lib/trailblazer/macro.rb
CHANGED
@@ -1,11 +1,29 @@
|
|
1
|
-
require "trailblazer/
|
1
|
+
require "trailblazer/activity"
|
2
|
+
require "trailblazer/activity/dsl/linear" # TODO: remove this dependency
|
3
|
+
require "trailblazer/operation" # TODO: remove this dependency
|
2
4
|
|
3
|
-
require "trailblazer/
|
5
|
+
require "trailblazer/macro/model"
|
6
|
+
require "trailblazer/macro/policy"
|
7
|
+
require "trailblazer/macro/guard"
|
8
|
+
require "trailblazer/macro/pundit"
|
9
|
+
require "trailblazer/macro/nested"
|
10
|
+
require "trailblazer/macro/rescue"
|
11
|
+
require "trailblazer/macro/wrap"
|
4
12
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
13
|
+
module Trailblazer
|
14
|
+
module Macro
|
15
|
+
# All macros sit in the {Trailblazer::Macro} namespace, where we forward calls from
|
16
|
+
# operations and activities to.
|
17
|
+
def self.forward_macros(target)
|
18
|
+
target.singleton_class.def_delegators Trailblazer::Macro, :Model, :Wrap, :Rescue, :Nested
|
19
|
+
target.const_set(:Policy, Trailblazer::Macro::Policy)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# TODO: Forwardable.def_delegators(Operation, Macro, :Model, :Wrap) would be amazing. It really sucks to extend a foreign class.
|
25
|
+
# Trailblazer::Operation.singleton_class.extend Forwardable
|
26
|
+
# Trailblazer::Macro.forward_macros(Trailblazer::Operation)
|
27
|
+
|
28
|
+
Trailblazer::Activity::FastTrack.singleton_class.extend Forwardable
|
29
|
+
Trailblazer::Macro.forward_macros(Trailblazer::Activity::FastTrack) # monkey-patching sucks.
|
@@ -1,7 +1,7 @@
|
|
1
|
-
|
1
|
+
module Trailblazer::Macro
|
2
2
|
module Policy
|
3
3
|
def self.Guard(proc, name: :default, &block)
|
4
|
-
Policy.step(
|
4
|
+
Policy.step(Guard.build(proc), name: name)
|
5
5
|
end
|
6
6
|
|
7
7
|
module Guard
|
@@ -9,8 +9,8 @@ class Trailblazer::Operation
|
|
9
9
|
option = Trailblazer::Option::KW(callable)
|
10
10
|
|
11
11
|
# this gets wrapped in a Operation::Result object.
|
12
|
-
->(
|
13
|
-
Result.new(!!option.call(options, circuit_args), {})
|
12
|
+
->((options, *), circuit_args) do
|
13
|
+
Trailblazer::Operation::Result.new(!!option.call(options, circuit_args), {})
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
@@ -1,23 +1,21 @@
|
|
1
|
-
|
2
|
-
def self.Model(model_class, action=nil, find_by_key=nil)
|
1
|
+
module Trailblazer::Macro
|
2
|
+
def self.Model(model_class, action = nil, find_by_key = nil)
|
3
3
|
task = Trailblazer::Activity::TaskBuilder::Binary(Model.new)
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
"model.find_by_key" => find_by_key
|
10
|
-
)
|
5
|
+
injection = Trailblazer::Activity::TaskWrap::Inject::Defaults::Extension(
|
6
|
+
"model.class" => model_class,
|
7
|
+
"model.action" => action,
|
8
|
+
"model.find_by_key" => find_by_key
|
11
9
|
)
|
12
10
|
|
13
|
-
{
|
11
|
+
{task: task, id: "model.build", extensions: [injection]}
|
14
12
|
end
|
15
13
|
|
16
14
|
class Model
|
17
15
|
def call(options, params:, **)
|
18
16
|
builder = Model::Builder.new
|
19
17
|
options[:model] = model = builder.call(options, params)
|
20
|
-
options["result.model"] = result = Result.new(!model.nil?, {})
|
18
|
+
options["result.model"] = result = Trailblazer::Operation::Result.new(!model.nil?, {})
|
21
19
|
|
22
20
|
result.success?
|
23
21
|
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# per default, everything we pass into a circuit is immutable. it's the ops/act's job to allow writing (via a Context)
|
2
|
+
module Trailblazer
|
3
|
+
module Macro
|
4
|
+
# {Nested} macro.
|
5
|
+
def self.Nested(callable, id: "Nested(#{callable})")
|
6
|
+
if callable.is_a?(Class) && callable < Nested.operation_class
|
7
|
+
warn %{[Trailblazer] Using the `Nested()` macro with operations and activities is deprecated. Replace `Nested(Create)` with `Subprocess(Create)`.}
|
8
|
+
return Nested.operation_class.Subprocess(callable)
|
9
|
+
end
|
10
|
+
|
11
|
+
# dynamic
|
12
|
+
task = Nested::Dynamic.new(callable)
|
13
|
+
|
14
|
+
merge = [
|
15
|
+
[Activity::TaskWrap::Pipeline.method(:insert_before), "task_wrap.call_task", ["Nested.compute_nested_activity", task.method(:compute_nested_activity)]],
|
16
|
+
[Activity::TaskWrap::Pipeline.method(:insert_after), "task_wrap.call_task", ["Nested.compute_return_signal", task.method(:compute_return_signal)]],
|
17
|
+
]
|
18
|
+
|
19
|
+
task_wrap_extension = Activity::TaskWrap::Extension(merge: merge)
|
20
|
+
|
21
|
+
{
|
22
|
+
task: task,
|
23
|
+
id: id,
|
24
|
+
extensions: [task_wrap_extension],
|
25
|
+
outputs: task.outputs,
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
# @private
|
30
|
+
module Nested
|
31
|
+
def self.operation_class
|
32
|
+
Operation
|
33
|
+
end
|
34
|
+
|
35
|
+
# For dynamic `Nested`s that do not expose an {Activity} interface.
|
36
|
+
# Since we do not know its outputs, we have to map them to :success and :failure, only.
|
37
|
+
#
|
38
|
+
# This is what {Nested} in 2.0 used to do, where the outcome could only be true/false (or success/failure).
|
39
|
+
class Dynamic
|
40
|
+
def initialize(nested_activity_decider)
|
41
|
+
@nested_activity_decider = Option::KW(nested_activity_decider)
|
42
|
+
|
43
|
+
@outputs = {
|
44
|
+
:success => Activity::Output(Activity::Railway::End::Success.new(semantic: :success), :success),
|
45
|
+
:failure => Activity::Output(Activity::Railway::End::Failure.new(semantic: :failure), :failure)
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_reader :outputs
|
50
|
+
|
51
|
+
# TaskWrap step.
|
52
|
+
def compute_nested_activity(wrap_ctx, original_args)
|
53
|
+
(ctx, _), original_circuit_options = original_args
|
54
|
+
|
55
|
+
# TODO: evaluate the option to get the actual "object" to call.
|
56
|
+
activity = @nested_activity_decider.(ctx, original_circuit_options)
|
57
|
+
|
58
|
+
# Overwrite :task so task_wrap.call_task will call this activity.
|
59
|
+
# This is a trick so we don't have to repeat logic from #call_task here.
|
60
|
+
wrap_ctx[:task] = activity
|
61
|
+
|
62
|
+
return wrap_ctx, original_args
|
63
|
+
end
|
64
|
+
|
65
|
+
def compute_return_signal(wrap_ctx, original_args)
|
66
|
+
# Translate the genuine nested signal to the generic Dynamic end (success/failure, only).
|
67
|
+
# Note that here we lose information about what specific event was emitted.
|
68
|
+
wrap_ctx[:return_signal] = wrap_ctx[:return_signal].kind_of?(Activity::Railway::End::Success) ?
|
69
|
+
@outputs[:success].signal : @outputs[:failure].signal
|
70
|
+
|
71
|
+
return wrap_ctx, original_args
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -1,9 +1,9 @@
|
|
1
|
-
|
1
|
+
module Trailblazer::Macro
|
2
2
|
module Policy
|
3
3
|
# Step: This generically `call`s a policy and then pushes its result to `options`.
|
4
4
|
# You can use any callable object as a policy with this step.
|
5
5
|
class Eval
|
6
|
-
def initialize(name:nil, path:nil)
|
6
|
+
def initialize(name: nil, path: nil)
|
7
7
|
@name = name
|
8
8
|
@path = path
|
9
9
|
end
|
@@ -11,8 +11,8 @@ class Trailblazer::Operation
|
|
11
11
|
# incoming low-level {Task API}.
|
12
12
|
# outgoing Task::Binary API.
|
13
13
|
def call((options, flow_options), **circuit_options)
|
14
|
-
condition = options[
|
15
|
-
result = condition.(
|
14
|
+
condition = options[@path] # this allows dependency injection.
|
15
|
+
result = condition.([options, flow_options], **circuit_options)
|
16
16
|
|
17
17
|
options["policy.#{@name}"] = result["policy"] # assign the policy as a skill.
|
18
18
|
options["result.policy.#{@name}"] = result
|
@@ -20,7 +20,7 @@ class Trailblazer::Operation
|
|
20
20
|
# flow control
|
21
21
|
signal = result.success? ? Trailblazer::Activity::Right : Trailblazer::Activity::Left # since we & this, it's only executed OnRight and the return boolean decides the direction, input is passed straight through.
|
22
22
|
|
23
|
-
return signal, [
|
23
|
+
return signal, [options, flow_options]
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
@@ -30,15 +30,13 @@ class Trailblazer::Operation
|
|
30
30
|
name = options[:name]
|
31
31
|
path = "policy.#{name}.eval"
|
32
32
|
|
33
|
-
task = Eval.new(
|
33
|
+
task = Eval.new(name: name, path: path)
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
path => condition
|
38
|
-
)
|
35
|
+
injection = Trailblazer::Activity::TaskWrap::Inject::Defaults::Extension(
|
36
|
+
path => condition
|
39
37
|
)
|
40
38
|
|
41
|
-
{
|
39
|
+
{task: task, id: path, extensions: [injection]}
|
42
40
|
end
|
43
41
|
end
|
44
42
|
end
|
@@ -1,7 +1,7 @@
|
|
1
|
-
|
1
|
+
module Trailblazer::Macro
|
2
2
|
module Policy
|
3
3
|
def self.Pundit(policy_class, action, name: :default)
|
4
|
-
Policy.step(
|
4
|
+
Policy.step(Pundit.build(policy_class, action), name: name)
|
5
5
|
end
|
6
6
|
|
7
7
|
module Pundit
|
@@ -17,7 +17,7 @@ class Trailblazer::Operation
|
|
17
17
|
|
18
18
|
# Instantiate the actual policy object, and call it.
|
19
19
|
def call((options), *)
|
20
|
-
policy = build_policy(options)
|
20
|
+
policy = build_policy(options) # this translates to Pundit interface.
|
21
21
|
result!(policy.send(@action), policy)
|
22
22
|
end
|
23
23
|
|
@@ -27,10 +27,10 @@ class Trailblazer::Operation
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def result!(success, policy)
|
30
|
-
data = {
|
30
|
+
data = {"policy" => policy}
|
31
31
|
data["message"] = "Breach" if !success # TODO: how to allow messages here?
|
32
32
|
|
33
|
-
Result.new(success, data)
|
33
|
+
Trailblazer::Operation::Result.new(success, data)
|
34
34
|
end
|
35
35
|
end
|
36
36
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
module Macro
|
3
|
+
NoopHandler = lambda { |*| }
|
4
|
+
|
5
|
+
def self.Rescue(*exceptions, handler: NoopHandler, &block)
|
6
|
+
exceptions = [StandardError] unless exceptions.any?
|
7
|
+
|
8
|
+
handler = Rescue.deprecate_positional_handler_signature(handler)
|
9
|
+
handler = Trailblazer::Option(handler)
|
10
|
+
|
11
|
+
# This block is evaluated by {Wrap}.
|
12
|
+
rescue_block = ->((ctx, flow_options), **circuit_options, &nested_activity) do
|
13
|
+
begin
|
14
|
+
nested_activity.call
|
15
|
+
rescue *exceptions => exception
|
16
|
+
# DISCUSS: should we deprecate this signature and rather apply the Task API here?
|
17
|
+
handler.call(exception, ctx, **circuit_options) # FIXME: when there's an error here, it shows the wrong exception!
|
18
|
+
|
19
|
+
[Operation::Railway.fail!, [ctx, flow_options]]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
Wrap(rescue_block, id: "Rescue(#{rand(100)})", &block)
|
24
|
+
# FIXME: name
|
25
|
+
# [ step, name: "Rescue:#{block.source_location.last}" ]
|
26
|
+
end
|
27
|
+
|
28
|
+
# TODO: remove me in 2.2.
|
29
|
+
module Rescue
|
30
|
+
def self.deprecate_positional_handler_signature(handler)
|
31
|
+
return handler if handler.is_a?(Symbol) # can't do nutting about this.
|
32
|
+
|
33
|
+
arity = handler.is_a?(Class) ? handler.method(:call).arity : handler.arity
|
34
|
+
|
35
|
+
return handler if arity != 2 # means (exception, (ctx, flow_options), *, &block), "new style"
|
36
|
+
|
37
|
+
->(exception, (ctx, flow_options), **circuit_options, &block) do
|
38
|
+
warn "[Trailblazer] Rescue handlers have a new signature: (exception, *, &block)"
|
39
|
+
handler.(exception, ctx, &block)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
module Macro
|
3
|
+
def self.Wrap(user_wrap, id: "Wrap/#{rand(100)}", &block)
|
4
|
+
activity = Class.new(Activity::FastTrack, &block) # This is currently coupled to {dsl-linear}.
|
5
|
+
|
6
|
+
outputs = activity.to_h[:outputs]
|
7
|
+
outputs = Hash[outputs.collect { |output| [output.semantic, output] }] # TODO: make that a helper somewhere.
|
8
|
+
|
9
|
+
wrapped = Wrap::Wrapped.new(activity, user_wrap, outputs)
|
10
|
+
|
11
|
+
{task: wrapped, id: id, outputs: outputs}
|
12
|
+
end
|
13
|
+
|
14
|
+
module Wrap
|
15
|
+
# behaves like an operation so it plays with Nested and simply calls the operation in the user-provided block.
|
16
|
+
class Wrapped
|
17
|
+
private def deprecate_positional_wrap_signature(user_wrap)
|
18
|
+
parameters = user_wrap.is_a?(Module) ? user_wrap.method(:call).parameters : user_wrap.parameters
|
19
|
+
|
20
|
+
return user_wrap if parameters[0] == [:req] # means ((ctx, flow_options), *, &block), "new style"
|
21
|
+
|
22
|
+
->((ctx, flow_options), **circuit_options, &block) do
|
23
|
+
warn "[Trailblazer] Wrap handlers have a new signature: ((ctx), *, &block)"
|
24
|
+
user_wrap.(ctx, &block)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(operation, user_wrap, outputs)
|
29
|
+
user_wrap = deprecate_positional_wrap_signature(user_wrap)
|
30
|
+
|
31
|
+
@operation = operation
|
32
|
+
@user_wrap = user_wrap
|
33
|
+
|
34
|
+
# Since in the user block, you can return Railway.pass! etc, we need to map
|
35
|
+
# those to the actual wrapped operation's end.
|
36
|
+
@signal_to_output = {
|
37
|
+
Operation::Railway.pass! => outputs[:success].signal,
|
38
|
+
Operation::Railway.fail! => outputs[:failure].signal,
|
39
|
+
Operation::Railway.pass_fast! => outputs[:pass_fast].signal,
|
40
|
+
Operation::Railway.fail_fast! => outputs[:fail_fast].signal,
|
41
|
+
true => outputs[:success].signal,
|
42
|
+
false => outputs[:failure].signal,
|
43
|
+
nil => outputs[:failure].signal,
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def call((ctx, flow_options), **circuit_options)
|
48
|
+
block_calling_wrapped = -> {
|
49
|
+
call_wrapped_activity([ctx, flow_options], circuit_options)
|
50
|
+
}
|
51
|
+
|
52
|
+
# call the user's Wrap {} block in the operation.
|
53
|
+
# This will invoke block_calling_wrapped above if the user block yields.
|
54
|
+
returned = @user_wrap.([ctx, flow_options], **circuit_options, &block_calling_wrapped)
|
55
|
+
|
56
|
+
# {returned} can be
|
57
|
+
# 1. {circuit interface return} from the begin block, because the wrapped OP passed
|
58
|
+
# 2. {task interface return} because the user block returns "customized" signals, true of fale
|
59
|
+
|
60
|
+
if returned.is_a?(Array) # 1. {circuit interface return}, new style.
|
61
|
+
signal, (ctx, flow_options) = returned
|
62
|
+
else # 2. {task interface return}, only a signal (or true/false)
|
63
|
+
# TODO: deprecate this?
|
64
|
+
signal = returned
|
65
|
+
end
|
66
|
+
|
67
|
+
# Use the original {signal} if there's no mapping.
|
68
|
+
# This usually means signal is an End instance or a custom signal.
|
69
|
+
signal = @signal_to_output.fetch(signal, signal)
|
70
|
+
|
71
|
+
return signal, [ctx, flow_options]
|
72
|
+
end
|
73
|
+
|
74
|
+
def call_wrapped_activity((ctx, flow_options), **circuit_options)
|
75
|
+
@operation.to_h[:activity].([ctx, flow_options], **circuit_options) # :exec_context is this instance.
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end # Wrap
|
79
|
+
end
|
80
|
+
end
|