trailblazer 2.0.7 → 2.1.0.beta1

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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +35 -1
  3. data/Gemfile +6 -12
  4. data/README.md +3 -1
  5. data/Rakefile +6 -17
  6. data/lib/trailblazer.rb +7 -4
  7. data/lib/trailblazer/deprecation/call.rb +46 -0
  8. data/lib/trailblazer/deprecation/context.rb +43 -0
  9. data/lib/trailblazer/operation/contract.rb +40 -9
  10. data/lib/trailblazer/operation/deprecations.rb +21 -0
  11. data/lib/trailblazer/operation/guard.rb +5 -5
  12. data/lib/trailblazer/operation/model.rb +15 -10
  13. data/lib/trailblazer/operation/nested.rb +56 -85
  14. data/lib/trailblazer/operation/persist.rb +4 -2
  15. data/lib/trailblazer/operation/policy.rb +16 -7
  16. data/lib/trailblazer/operation/pundit.rb +3 -3
  17. data/lib/trailblazer/operation/representer.rb +5 -0
  18. data/lib/trailblazer/operation/rescue.rb +12 -9
  19. data/lib/trailblazer/operation/validate.rb +36 -29
  20. data/lib/trailblazer/operation/wrap.rb +49 -11
  21. data/lib/trailblazer/task.rb +20 -0
  22. data/lib/trailblazer/version.rb +1 -1
  23. data/test/benchmark.rb +63 -0
  24. data/test/deprecation/call_test.rb +42 -0
  25. data/test/deprecation/context_test.rb +19 -0
  26. data/test/docs/contract_test.rb +73 -53
  27. data/test/docs/dry_test.rb +2 -2
  28. data/test/docs/fast_test.rb +133 -13
  29. data/test/docs/guard_test.rb +28 -35
  30. data/test/docs/macro_test.rb +1 -1
  31. data/test/docs/model_test.rb +13 -13
  32. data/test/docs/nested_test.rb +54 -122
  33. data/test/docs/operation_test.rb +42 -43
  34. data/test/docs/pundit_test.rb +16 -16
  35. data/test/docs/representer_test.rb +18 -18
  36. data/test/docs/rescue_test.rb +29 -29
  37. data/test/docs/trace_test.rb +82 -0
  38. data/test/docs/wrap_test.rb +59 -26
  39. data/test/module_test.rb +75 -75
  40. data/test/nested_test.rb +293 -0
  41. data/test/operation/contract_test.rb +23 -153
  42. data/test/operation/dsl/contract_test.rb +9 -9
  43. data/test/operation/dsl/representer_test.rb +169 -169
  44. data/test/operation/model_test.rb +15 -21
  45. data/test/operation/persist_test.rb +18 -11
  46. data/test/operation/pundit_test.rb +25 -23
  47. data/test/operation/representer_test.rb +254 -254
  48. data/test/test_helper.rb +5 -2
  49. data/test/variables_test.rb +158 -0
  50. data/trailblazer.gemspec +1 -1
  51. data/untitled +33 -0
  52. metadata +25 -27
  53. data/lib/trailblazer/operation/callback.rb +0 -35
  54. data/lib/trailblazer/operation/procedural/contract.rb +0 -15
  55. data/lib/trailblazer/operation/procedural/validate.rb +0 -22
  56. data/test/operation/callback_test.rb +0 -70
  57. data/test/operation/dsl/callback_test.rb +0 -106
  58. data/test/operation/params_test.rb +0 -36
  59. data/test/operation/pipedream_test.rb +0 -59
  60. data/test/operation/pipetree_test.rb +0 -104
  61. data/test/operation/present_test.rb +0 -24
  62. data/test/operation/resolver_test.rb +0 -47
  63. data/test/operation_test.rb +0 -143
@@ -2,9 +2,11 @@ class Trailblazer::Operation
2
2
  module Contract
3
3
  def self.Persist(method: :save, name: "default")
4
4
  path = "contract.#{name}"
5
- step = ->(input, options) { options[path].send(method) }
5
+ step = ->(options, **) { options[path].send(method) }
6
6
 
7
- [ step, name: "persist.save" ]
7
+ task = Railway::TaskBuilder.( step )
8
+
9
+ { task: task, id: "persist.save" }
8
10
  end
9
11
  end
10
12
  end
@@ -8,15 +8,19 @@ class Trailblazer::Operation
8
8
  @path = path
9
9
  end
10
10
 
11
- def call(input, options)
12
- condition = options[@path] # this allows dependency injection.
13
- result = condition.(input, options)
11
+ # incoming low-level {Task API}.
12
+ # outgoing Task::Binary API.
13
+ def call((options, flow_options), **circuit_options)
14
+ condition = options[ @path ] # this allows dependency injection.
15
+ result = condition.( [options, flow_options], **circuit_options )
14
16
 
15
17
  options["policy.#{@name}"] = result["policy"] # assign the policy as a skill.
16
18
  options["result.policy.#{@name}"] = result
17
19
 
18
20
  # flow control
19
- result.success? # since we & this, it's only executed OnRight and the return boolean decides the direction, input is passed straight through.
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
+
23
+ return signal, [ options, flow_options ]
20
24
  end
21
25
  end
22
26
 
@@ -26,10 +30,15 @@ class Trailblazer::Operation
26
30
  name = options[:name]
27
31
  path = "policy.#{name}.eval"
28
32
 
29
- step = Eval.new( name: name, path: path )
30
- step = Pipetree::Step.new(step, path => condition)
33
+ task = Eval.new( name: name, path: path )
34
+
35
+ runner_options = {
36
+ merge: Trailblazer::Operation::Wrap::Inject::Defaults(
37
+ path => condition
38
+ )
39
+ }
31
40
 
32
- [ step, name: path ]
41
+ { task: task, id: path, runner_options: runner_options }
33
42
  end
34
43
  end
35
44
  end
@@ -1,7 +1,7 @@
1
1
  class Trailblazer::Operation
2
2
  module Policy
3
3
  def self.Pundit(policy_class, action, name: :default)
4
- Policy.step(Pundit.build(policy_class, action), name: name)
4
+ Policy.step( Pundit.build(policy_class, action), name: name )
5
5
  end
6
6
 
7
7
  module Pundit
@@ -16,14 +16,14 @@ class Trailblazer::Operation
16
16
  end
17
17
 
18
18
  # Instantiate the actual policy object, and call it.
19
- def call(input, options)
19
+ def call((options), *)
20
20
  policy = build_policy(options) # this translates to Pundit interface.
21
21
  result!(policy.send(@action), policy)
22
22
  end
23
23
 
24
24
  private
25
25
  def build_policy(options)
26
- @policy_class.new(options["current_user"], options["model"])
26
+ @policy_class.new(options[:current_user], options[:model])
27
27
  end
28
28
 
29
29
  def result!(success, policy)
@@ -12,6 +12,11 @@ class Trailblazer::Operation
12
12
  end
13
13
 
14
14
  module DSL
15
+ def self.extended(extender)
16
+ extender.extend(ClassDependencies)
17
+ warn "[Trailblazer] Using `representer do...end` is deprecated. Please use a dedicated representer class."
18
+ end
19
+
15
20
  def representer(name=:default, constant=nil, &block)
16
21
  heritage.record(:representer, name, constant, &block)
17
22
 
@@ -1,21 +1,24 @@
1
1
  class Trailblazer::Operation
2
- def self.Rescue(*exceptions, handler: lambda { |*| }, &block)
2
+ NoopHandler = lambda { |*| }
3
+
4
+ def self.Rescue(*exceptions, handler: NoopHandler, &block)
3
5
  exceptions = [StandardError] unless exceptions.any?
4
- handler = Option.(handler)
6
+ handler = Trailblazer::Option(handler)
5
7
 
6
- rescue_block = ->(options, operation, *, &nested_pipe) {
8
+ # This block is evaluated by {Wrap} which currently expects a binary return type.
9
+ rescue_block = ->(options, flow_options, **circuit_options, &nested_activity) {
7
10
  begin
8
- res = nested_pipe.call
9
- res.first == ::Pipetree::Railway::Right # FIXME.
11
+ nested_activity.call
10
12
  rescue *exceptions => exception
11
- handler.call(operation, exception, options)
13
+ # DISCUSS: should we deprecate this signature and rather apply the Task API here?
14
+ handler.call(exception, options, **circuit_options) # FIXME: when there's an error here, it shows the wrong exception!
12
15
  false
13
16
  end
14
17
  }
15
18
 
16
- step, _ = Wrap(rescue_block, &block)
17
-
18
- [ step, name: "Rescue:#{block.source_location.last}" ]
19
+ Wrap(rescue_block, id: "Rescue(#{rand(100)})", &block)
20
+ # FIXME: name
21
+ # [ step, name: "Rescue:#{block.source_location.last}" ]
19
22
  end
20
23
  end
21
24
 
@@ -1,58 +1,65 @@
1
1
  class Trailblazer::Operation
2
2
  module Contract
3
- Railway = Pipetree::Railway
4
-
5
3
  # result.contract = {..}
6
4
  # result.contract.errors = {..}
7
5
  # Deviate to left track if optional key is not found in params.
8
6
  # Deviate to left if validation result falsey.
9
- def self.Validate(skip_extract:false, name: "default", representer:false, key: nil) # DISCUSS: should we introduce something like Validate::Deserializer?
7
+ def self.Validate(skip_extract: false, name: "default", representer: false, key: nil) # DISCUSS: should we introduce something like Validate::Deserializer?
10
8
  params_path = "contract.#{name}.params" # extract_params! save extracted params here.
11
9
 
12
- return Validate::Call(name: name, representer: representer, params_path: params_path) if skip_extract || representer
10
+ extract = Validate::Extract.new( key: key, params_path: params_path ).freeze
11
+ validate = Validate.new( name: name, representer: representer, params_path: params_path ).freeze
13
12
 
14
- extract_step, extract_options = Validate::Extract(key: key, params_path: params_path)
15
- validate_step, validate_options = Validate::Call(name: name, representer: representer, params_path: params_path)
13
+ # Return the Validate::Call task if the first step, the params extraction, is not desired.
14
+ if skip_extract || representer
15
+ return { task: Trailblazer::Activity::Task::Binary( validate ), id: "contract.#{name}.call" }
16
+ end
16
17
 
17
- pipe = Railway.new # TODO: make nested pipes simpler.
18
- .add(Railway::Right, Railway.&(extract_step), extract_options)
19
- .add(Railway::Right, Railway.&(validate_step), validate_options)
20
18
 
21
- step = ->(input, options) { pipe.(input, options).first <= Railway::Right }
19
+ # Build a simple Railway {Activity} for the internal flow.
20
+ activity = Trailblazer::Activity::Railway.build do # FIXME: make Activity.build(builder: Railway) do end an <Activity>
21
+ step Trailblazer::Activity::Task::Binary( extract ), id: "#{params_path}_extract"
22
+ step Trailblazer::Activity::Task::Binary( validate ), id: "contract.#{name}.call"
23
+ end
22
24
 
23
- [step, name: "contract.#{name}.validate"]
25
+ # DISCUSS: use Nested here?
26
+ # Nested.operation_class.Nested( activity, id: "contract.#{name}.validate" )
27
+ { task: activity, id: "contract.#{name}.validate", plus_poles: Trailblazer::Activity::Magnetic::DSL::PlusPoles.from_outputs(activity.outputs) }
24
28
  end
25
29
 
26
- module Validate
27
- # Macro: extract the contract's input from params by reading `:key`.
28
- def self.Extract(key:nil, params_path:nil)
29
- # TODO: introduce nested pipes and pass composed input instead.
30
- step = ->(input, options) do
31
- options[params_path] = key ? options["params"][key] : options["params"]
30
+ class Validate
31
+ # Task: extract the contract's input from params by reading `:key`.
32
+ class Extract
33
+ def initialize(key:nil, params_path:nil)
34
+ @key, @params_path = key, params_path
32
35
  end
33
36
 
34
- [ step, name: params_path ]
37
+ def call( (options, flow_options), **circuit_options )
38
+ options[@params_path] = @key ? options[:params][@key] : options[:params]
39
+ end
35
40
  end
36
41
 
37
- # Macro: Validates contract `:name`.
38
- def self.Call(name:"default", representer:false, params_path:nil)
39
- step = ->(input, options) {
40
- validate!(options, name: name, representer: options["representer.#{name}.class"], params_path: params_path)
41
- }
42
-
43
- step = Pipetree::Step.new( step, "representer.#{name}.class" => representer )
42
+ def initialize(name:"default", representer:false, params_path:nil)
43
+ @name, @representer, @params_path = name, representer, params_path
44
+ end
44
45
 
45
- [ step, name: "contract.#{name}.call" ]
46
+ # Task: Validates contract `:name`.
47
+ def call( (options, flow_options), **circuit_options )
48
+ validate!(
49
+ options,
50
+ representer: options["representer.#{@name}.class"] ||= @representer, # FIXME: maybe @representer should use DI.
51
+ params_path: @params_path
52
+ )
46
53
  end
47
54
 
48
- def self.validate!(options, name:nil, representer:false, from: "document", params_path:nil)
49
- path = "contract.#{name}"
55
+ def validate!(options, representer:false, from: :document, params_path:nil)
56
+ path = "contract.#{@name}"
50
57
  contract = options[path]
51
58
 
52
59
  # this is for 1.1-style compatibility and should be removed once we have Deserializer in place:
53
60
  options["result.#{path}"] = result =
54
61
  if representer
55
- # use "document" as the body and let the representer deserialize to the contract.
62
+ # use :document as the body and let the representer deserialize to the contract.
56
63
  # this will be simplified once we have Deserializer.
57
64
  # translates to contract.("{document: bla}") { MyRepresenter.new(contract).from_json .. }
58
65
  contract.(options[from]) { |document| representer.new(contract).parse(document) }
@@ -1,22 +1,60 @@
1
1
  class Trailblazer::Operation
2
- Base = self # TODO: we won't need this with 2.1.
3
2
 
4
- def self.Wrap(wrap, &block)
5
- operation = Class.new(Base)
6
- # DISCUSS: don't instance_exec when |pipe| given?
7
- operation.instance_exec(&block) # evaluate the nested pipe.
3
+ # false is automatically connected to End.failure.
8
4
 
9
- pipe = operation["pipetree"]
10
- pipe.add(nil, nil, {delete: "operation.new"}) # TODO: make this a bit more elegant.
5
+ def self.Wrap(user_wrap, id: "Wrap/#{rand(100)}", &block)
6
+ operation_class = Wrap.Operation(block)
7
+ wrapped = Wrap::Wrapped.new(operation_class, user_wrap)
11
8
 
12
- step = Wrap.for(wrap, pipe)
9
+ # connect `false` as an end event, when an exception stopped the wrap, for example.
13
10
 
14
- [ step, {} ]
11
+ { task: wrapped, id: id, plus_poles: Trailblazer::Activity::Magnetic::DSL::PlusPoles.from_outputs(operation_class.outputs) }
12
+ # TODO: Nested could have a better API and do the "merge" for us!
15
13
  end
16
14
 
17
15
  module Wrap
18
- def self.for(wrap, pipe)
19
- ->(input, options) { wrap.(options, input, pipe, & ->{ pipe.(input, options) }) }
16
+ def self.Operation(block)
17
+ Class.new( Nested.operation_class, &block ) # Usually resolves to Trailblazer::Operation.
18
+ end
19
+
20
+ # behaves like an operation so it plays with Nested and simply calls the operation in the user-provided block.
21
+ class Wrapped #< Trailblazer::Operation # FIXME: the inheritance is only to satisfy Nested( Wrapped.new )
22
+ include Trailblazer::Activity::Interface
23
+
24
+ def initialize(operation, user_wrap)
25
+ @operation = operation
26
+ @user_wrap = user_wrap
27
+ end
28
+
29
+ def call( (options, flow_options), **circuit_options )
30
+ block_calling_wrapped = -> {
31
+ args, circuit_options_with_wrap_static = Railway::TaskWrap.arguments_for_call( @operation, [options, flow_options], **circuit_options )
32
+
33
+ # TODO: this is not so nice, still working out how to separate all those bits and pieces.
34
+ @operation.instance_variable_get(:@process).( args, **circuit_options.merge(circuit_options_with_wrap_static) ) # FIXME: arguments_for_call don't return the full circuit_options, :exec_context gets lost.
35
+ }
36
+
37
+ # call the user's Wrap {} block in the operation.
38
+ # This will invoke block_calling_wrapped above if the user block yields.
39
+ returned = @user_wrap.( options, flow_options, **circuit_options, &block_calling_wrapped )
40
+
41
+ # returned could be
42
+ # 1. the 1..>=3 Task interface result
43
+ # 2. false
44
+ # 3. true or something else, but not the Task interface (when rescue isn't needed)
45
+
46
+ # legacy outcome.
47
+ # FIXME: we *might* return some "older version" of options here!
48
+ if returned === false
49
+ return @operation.outputs[:failure].signal, [options, flow_options]
50
+ end
51
+
52
+ returned # let's hope returned is one of activity's Ends.
53
+ end
54
+
55
+ def outputs
56
+ @operation.outputs # FIXME: we don't map false, yet
57
+ end
20
58
  end
21
59
  end # Wrap
22
60
  end
@@ -0,0 +1,20 @@
1
+ module Trailblazer
2
+ class Activity
3
+ module Task
4
+ # Convenience functions for tasks. Totally optional.
5
+
6
+ # Task::Binary aka "step"
7
+ # Step is binary task: true=> Right, false=>Left.
8
+ # Step call proc.(options, flow_options)
9
+ # Step is supposed to run Option::KW, so `step` should be Option::KW.
10
+ #
11
+ # Returns task to call the proc with (options, flow_options), omitting `direction`.
12
+ # When called, the task always returns a direction signal.
13
+ def self.Binary(step, on_true=Activity::Right, on_false=Activity::Left)
14
+ ->(*args) do # Activity/Task interface.
15
+ [ step.(*args) ? on_true : on_false, *args ] # <=> Activity/Task interface
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,3 +1,3 @@
1
1
  module Trailblazer
2
- VERSION = "2.0.7"
2
+ VERSION = "2.1.0.beta1"
3
3
  end
@@ -0,0 +1,63 @@
1
+ require "test_helper"
2
+
3
+ require "benchmark/ips"
4
+
5
+
6
+ def positional_vs_decompose
7
+ arg = [ [1, 2], 3 ]
8
+
9
+ def a((one, two), three)
10
+ one + two + three
11
+ end
12
+
13
+ def b(arr, three)
14
+ arr[0] + arr[1] + three
15
+ end
16
+
17
+ Benchmark.ips do |x|
18
+ x.report("decompose") { a(*arg) }
19
+ x.report("positional") { b(*arg) }
20
+ x.compare!
21
+ end
22
+
23
+ # decompose 5.646M (± 3.1%) i/s - 28.294M in 5.015759s
24
+ # positional 5.629M (± 8.0%) i/s - 28.069M in 5.029745s
25
+ end
26
+
27
+ # positional_vs_decompose
28
+
29
+ ##########################################################
30
+
31
+ class Callable
32
+ def initialize(proc, i)
33
+ @proc = proc
34
+ @i = i
35
+ end
36
+
37
+ def call(args)
38
+ @proc.(@i, args)
39
+ end
40
+ end
41
+
42
+ def scoped(proc, i)
43
+ ->(args) { proc.(i, args) }
44
+ end
45
+
46
+ def object_vs_method_scope
47
+ proc = ->(i, args) { i + args }
48
+
49
+ callable = Callable.new(proc, 2)
50
+ scoped = scoped(proc, 2)
51
+
52
+ Benchmark.ips do |x|
53
+ x.report("callable") { callable.(1) }
54
+ x.report("scoped") { scoped.(1) }
55
+ x.compare!
56
+ end
57
+ end
58
+
59
+ object_vs_method_scope
60
+
61
+ # Comparison:
62
+ # callable: 4620906.3 i/s
63
+ # scoped: 3388535.6 i/s - 1.36x slower
@@ -0,0 +1,42 @@
1
+ require "test_helper"
2
+
3
+ require "trailblazer/deprecation/context"
4
+ require "trailblazer/deprecation/call"
5
+
6
+ class DeprecationCallTest < Minitest::Spec
7
+ class Create < Trailblazer::Operation
8
+ step :create_model
9
+
10
+ def create_model(options, params:, **)
11
+ options["model"] = params.inspect
12
+ options[:user] = options["current_user"]
13
+ end
14
+ end
15
+
16
+ it "works with correct style" do
17
+ result = Create.( params: { title: "Hello" } )
18
+ result.inspect(:model, :user, :current_user, :params).must_equal %{<Result:false ["{:title=>\\\"Hello\\\"}", nil, nil, {:title=>\"Hello\"}] >}
19
+ end
20
+
21
+ it "works with correct style plus dependencies" do
22
+ result = Create.( params: { title: "Hello" }, current_user: Object )
23
+ result.inspect(:model, :user, :current_user, :params).must_equal %{<Result:true ["{:title=>\\\"Hello\\\"}", Object, Object, {:title=>\"Hello\"}] >}
24
+ end
25
+
26
+ it "converts old positional style" do
27
+ result = Create.( { title: "Hello" }, "current_user" => user=Object )
28
+ result.inspect(:model, :user, :current_user, :params).must_equal %{<Result:true ["{:title=>\\\"Hello\\\"}", Object, Object, {:title=>\"Hello\"}] >}
29
+ end
30
+
31
+ class WeirdStrongParameters < Hash
32
+ end
33
+
34
+ it "converts old positional style with StrongParameters" do
35
+ params = WeirdStrongParameters.new
36
+ params[:title] = "Hello"
37
+
38
+ result = Create.( params, "current_user" => user=Object )
39
+
40
+ result.inspect(:model, :user, :current_user, :params).must_equal %{<Result:true ["{:title=>\\\"Hello\\\"}", Object, Object, {:title=>\"Hello\"}] >}
41
+ end
42
+ end
@@ -0,0 +1,19 @@
1
+ require "test_helper"
2
+
3
+ require "trailblazer/deprecation/context"
4
+
5
+ class DeprecationContextTest < Minitest::Spec
6
+ class Create < Trailblazer::Operation
7
+ step :create_model
8
+
9
+ def create_model(options, params:, **)
10
+ options["model"] = params.inspect
11
+ options[:user] = options["current_user"]
12
+ end
13
+ end
14
+
15
+ it do
16
+ result = Create.( "params"=> {title: "Hello"}, "current_user" => user=Object)
17
+ result.inspect(:model, :user, :current_user, :params).must_equal %{<Result:true ["{:title=>\\\"Hello\\\"}", Object, Object, {:title=>\"Hello\"}] >}
18
+ end
19
+ end