trailblazer 2.0.0 → 2.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +13 -0
  3. data/COMM-LICENSE +21 -21
  4. data/Gemfile +3 -0
  5. data/README.md +15 -77
  6. data/Rakefile +1 -2
  7. data/doc/operation-2017.png +0 -0
  8. data/lib/trailblazer.rb +0 -1
  9. data/lib/trailblazer/operation/callback.rb +10 -19
  10. data/lib/trailblazer/operation/contract.rb +7 -8
  11. data/lib/trailblazer/operation/guard.rb +4 -13
  12. data/lib/trailblazer/operation/model.rb +30 -32
  13. data/lib/trailblazer/operation/nested.rb +21 -32
  14. data/lib/trailblazer/operation/persist.rb +4 -8
  15. data/lib/trailblazer/operation/policy.rb +5 -15
  16. data/lib/trailblazer/operation/pundit.rb +8 -17
  17. data/lib/trailblazer/operation/rescue.rb +17 -17
  18. data/lib/trailblazer/operation/validate.rb +34 -21
  19. data/lib/trailblazer/operation/wrap.rb +12 -25
  20. data/lib/trailblazer/version.rb +1 -1
  21. data/test/docs/dry_test.rb +4 -4
  22. data/test/docs/fast_test.rb +164 -0
  23. data/test/docs/guard_test.rb +2 -2
  24. data/test/docs/macro_test.rb +36 -0
  25. data/test/docs/model_test.rb +52 -0
  26. data/test/docs/nested_test.rb +101 -4
  27. data/test/docs/operation_test.rb +225 -1
  28. data/test/docs/pundit_test.rb +18 -17
  29. data/test/docs/rescue_test.rb +3 -3
  30. data/test/docs/wrap_test.rb +1 -0
  31. data/test/operation/callback_test.rb +5 -5
  32. data/test/operation/contract_test.rb +30 -19
  33. data/test/operation/dsl/callback_test.rb +2 -2
  34. data/test/operation/dsl/contract_test.rb +4 -4
  35. data/test/operation/model_test.rb +12 -57
  36. data/test/operation/persist_test.rb +7 -7
  37. data/test/operation/pipedream_test.rb +9 -9
  38. data/test/operation/pipetree_test.rb +5 -5
  39. data/test/operation/pundit_test.rb +7 -7
  40. data/test/operation/resolver_test.rb +47 -47
  41. data/trailblazer.gemspec +1 -1
  42. metadata +18 -11
  43. data/lib/trailblazer/operation/builder.rb +0 -24
  44. data/lib/trailblazer/operation/resolver.rb +0 -22
  45. data/test/controller_test.rb +0 -115
  46. data/test/operation/builder_test.rb +0 -89
@@ -1,43 +1,32 @@
1
1
  class Trailblazer::Operation
2
- module Nested
3
- # Please note that the instance_variable_get are here on purpose since the
4
- # superinternal API is not entirely decided, yet.
5
- def self.import!(operation, import, step)
6
- import.(:&, ->(input, options) {
7
- result = step._call(*options.to_runtime_data)
2
+ def self.Nested(step)
3
+ step = Nested.for(step)
8
4
 
9
- result.instance_variable_get(:@data).to_mutable_data.each do |k,v|
10
- options[k] = v
11
- end
12
-
13
- result.success? # DISCUSS: what if we could simply return the result object here?
14
- }, {} )
15
- end
5
+ [ step, {} ]
16
6
  end
17
7
 
18
- DSL.macro!(:Nested, Nested)
8
+ module Nested
9
+ def self.proc_for(step)
10
+ if step.is_a?(Class) && step <= Trailblazer::Operation # interestingly, with < we get a weird nil exception. bug in Ruby?
11
+ proc = ->(input, options) { step._call(*options.to_runtime_data) }
12
+ else
13
+ option = Option::KW.(step)
14
+ proc = ->(input, options) { option.(input, options).(*options.to_runtime_data) }
15
+ end
16
+ end
19
17
 
20
- module Rescue
21
- def self.import!(_operation, import, *exceptions, handler:->(*){}, &block)
22
- exceptions = [StandardError] unless exceptions.any?
23
- handler = Pipetree::DSL::Option.(handler)
18
+ # Please note that the instance_variable_get are here on purpose since the
19
+ # superinternal API is not entirely decided, yet.
20
+ def self.for(step)
21
+ proc = proc_for(step)
24
22
 
25
- rescue_block = ->(options, operation, *, &nested_pipe) {
26
- begin
27
- res = nested_pipe.call
28
- res.first == ::Pipetree::Flow::Right # FIXME.
29
- rescue *exceptions => exception
30
- handler.call(operation, exception, options)
31
- #options["result.model.find"] = "argh! because #{exception.class}"
32
- false
33
- end
34
- }
23
+ ->(input, options) do
24
+ result = proc.(input, options) # TODO: what about containers?
35
25
 
36
- # operation.| operation.Wrap(rescue_block, &block), name: "Rescue:#{block.source_location.last}"
37
- Wrap.import! _operation, import, rescue_block, name: "Rescue:#{block.source_location.last}", &block
26
+ result.instance_variable_get(:@data).to_mutable_data.each { |k,v| options[k] = v }
27
+ result.success? # DISCUSS: what if we could simply return the result object here?
28
+ end
38
29
  end
39
30
  end
40
-
41
- DSL.macro!(:Rescue, Rescue)
42
31
  end
43
32
 
@@ -1,14 +1,10 @@
1
1
  class Trailblazer::Operation
2
2
  module Contract
3
- module Persist
4
- def self.import!(operation, import, method: :save, name: "default")
5
- path = "contract.#{name}"
3
+ def self.Persist(method: :save, name: "default")
4
+ path = "contract.#{name}"
5
+ step = ->(input, options) { options[path].send(method) }
6
6
 
7
- import.(:&, ->(input, options) { options[path].send(method) }, # TODO: test me.
8
- name: "persist.save")
9
- end
7
+ [ step, name: "persist.save" ]
10
8
  end
11
9
  end
12
-
13
- DSL.macro!(:Persist, Contract::Persist, Contract.singleton_class) # Contract::Persist()
14
10
  end
@@ -3,8 +3,6 @@ class Trailblazer::Operation
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
- include Uber::Callable
7
-
8
6
  def initialize(name:nil, path:nil)
9
7
  @name = name
10
8
  @path = path
@@ -24,22 +22,14 @@ class Trailblazer::Operation
24
22
 
25
23
  # Adds the `yield` result to the pipe and treats it like a
26
24
  # policy-compatible object at runtime.
27
- def self.add!(operation, import, options, insert_options, &block)
25
+ def self.step(condition, options, &block)
28
26
  name = options[:name]
29
- path = options[:path]
30
-
31
- configure!(operation, import, options, &block)
27
+ path = "policy.#{name}.eval"
32
28
 
33
- # add step.
34
- import.(:&, Eval.new( name: name, path: path ),
35
- insert_options.merge(name: path)
36
- )
37
- end
29
+ step = Eval.new( name: name, path: path )
30
+ step = Pipetree::Step.new(step, path => condition)
38
31
 
39
- private
40
- def self.configure!(operation, import, options)
41
- # configure class level.
42
- operation[ options[:path] ] = yield
32
+ [ step, name: path ]
43
33
  end
44
34
  end
45
35
  end
@@ -1,10 +1,10 @@
1
1
  class Trailblazer::Operation
2
2
  module Policy
3
- module Pundit
4
- def self.import!(operation, import, policy_class, action, options={}, insert_options={})
5
- Policy.add!(operation, import, options, insert_options) { Pundit.build(policy_class, action) }
6
- end
3
+ def self.Pundit(policy_class, action, name: :default)
4
+ Policy.step(Pundit.build(policy_class, action), name: name)
5
+ end
7
6
 
7
+ module Pundit
8
8
  def self.build(*args, &block)
9
9
  Condition.new(*args, &block)
10
10
  end
@@ -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(skills)
20
- policy = build_policy(skills) # this translates to Pundit interface.
19
+ def call(options)
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
- def build_policy(skills)
26
- @policy_class.new(skills["current_user"], skills["model"])
25
+ def build_policy(options)
26
+ @policy_class.new(options["current_user"], options["model"])
27
27
  end
28
28
 
29
29
  def result!(success, policy)
@@ -34,14 +34,5 @@ class Trailblazer::Operation
34
34
  end
35
35
  end
36
36
  end
37
-
38
- def self.Pundit(policy, condition, name: :default, &block)
39
- options = {
40
- name: name,
41
- path: "policy.#{name}.eval",
42
- }
43
-
44
- [Pundit, [policy, condition, options], block]
45
- end
46
37
  end
47
38
  end
@@ -1,25 +1,25 @@
1
1
  class Trailblazer::Operation
2
- module Rescue
3
- Noop = ->(*) {}
2
+ def self.Rescue(*exceptions, handler: Rescue::Noop, &block)
3
+ exceptions = [StandardError] unless exceptions.any?
4
+ handler = Option.(handler)
4
5
 
5
- def self.import!(_operation, import, *exceptions, handler: Noop, &block)
6
- exceptions = [StandardError] unless exceptions.any?
7
- handler = Option.(handler)
6
+ rescue_block = ->(options, operation, *, &nested_pipe) {
7
+ begin
8
+ res = nested_pipe.call
9
+ res.first == ::Pipetree::Railway::Right # FIXME.
10
+ rescue *exceptions => exception
11
+ handler.call(operation, exception, options)
12
+ false
13
+ end
14
+ }
8
15
 
9
- rescue_block = ->(options, operation, *, &nested_pipe) {
10
- begin
11
- res = nested_pipe.call
12
- res.first == ::Pipetree::Flow::Right # FIXME.
13
- rescue *exceptions => exception
14
- handler.call(operation, exception, options)
15
- false
16
- end
17
- }
16
+ step, _ = Wrap(rescue_block, &block)
18
17
 
19
- Wrap.import! _operation, import, rescue_block, name: "Rescue:#{block.source_location.last}", &block
20
- end
18
+ [ step, name: "Rescue:#{block.source_location.last}" ]
21
19
  end
22
20
 
23
- DSL.macro!(:Rescue, Rescue)
21
+ module Rescue
22
+ Noop = ->(*) {}
23
+ end
24
24
  end
25
25
 
@@ -1,36 +1,51 @@
1
1
  class Trailblazer::Operation
2
2
  module Contract
3
+ Railway = Pipetree::Railway
4
+
3
5
  # result.contract = {..}
4
6
  # result.contract.errors = {..}
5
7
  # Deviate to left track if optional key is not found in params.
6
8
  # 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?
10
+ params_path = "contract.#{name}.params" # extract_params! save extracted params here.
11
+
12
+ return Validate::Call(name: name, representer: representer, params_path: params_path) if skip_extract || representer
13
+
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)
16
+
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
+
21
+ step = ->(input, options) { pipe.(input, options).first <= Railway::Right }
22
+
23
+ [step, name: "contract.#{name}.validate"]
24
+ end
25
+
7
26
  module Validate
8
- def self.import!(operation, import, skip_extract:false, name: "default", representer:false, key: nil) # DISCUSS: should we introduce something like Validate::Deserializer?
9
- if representer
10
- skip_extract = true
11
- operation["representer.#{name}.class"] = representer
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"]
12
32
  end
13
33
 
14
- params_path = "contract.#{name}.params" # extract_params! save extracted params here.
34
+ [ step, name: params_path ]
35
+ end
15
36
 
16
- import.(:&, ->(input, options) { extract_params!(input, options, key: key, path: params_path) },
17
- name: params_path) unless skip_extract
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
+ }
18
42
 
19
- # call the actual contract.validate(params)
20
- # DISCUSS: should we pass the representer here, or do that in #validate! i'm still mulling over what's the best, most generic approach.
21
- import.(:&, ->(operation, options) do
22
- validate!(operation, options, name: name, representer: options["representer.#{name}.class"], key: key, params_path: params_path)
23
- end,
24
- name: "contract.#{name}.validate", # visible name of the pipe step.
25
- )
26
- end
43
+ step = Pipetree::Step.new( step, "representer.#{name}.class" => representer )
27
44
 
28
- def self.extract_params!(operation, options, key:nil, path:nil)
29
- # TODO: introduce nested pipes and pass composed input instead.
30
- options[path] = key ? options["params"][key] : options["params"]
45
+ [ step, name: "contract.#{name}.call" ]
31
46
  end
32
47
 
33
- def self.validate!(operation, options, name: nil, representer:false, from: "document", params_path:nil, **)
48
+ def self.validate!(options, name:nil, representer:false, from: "document", params_path:nil)
34
49
  path = "contract.#{name}"
35
50
  contract = options[path]
36
51
 
@@ -50,6 +65,4 @@ class Trailblazer::Operation
50
65
  end
51
66
  end
52
67
  end
53
-
54
- DSL.macro!(:Validate, Contract::Validate, Contract.singleton_class)
55
68
  end
@@ -1,35 +1,22 @@
1
1
  class Trailblazer::Operation
2
- module Wrap
3
- def self.import!(operation, import, wrap, _options={}, &block)
4
- pipe_api = API.new(operation, pipe = ::Pipetree::Flow.new)
5
-
6
- # DISCUSS: don't instance_exec when |pipe| given?
7
- # yield pipe_api # create the nested pipe.
8
- pipe_api.instance_exec(&block) # evaluate the nested pipe. this gets written onto `pipe`.
9
-
10
- import.(:&, ->(input, options) { wrap.(options, input, pipe, & ->{ pipe.(input, options) }) }, _options)
11
- end
2
+ def self.Wrap(wrap, &block)
3
+ operation = Class.new(Trailblazer::Operation) # DISCUSS: Trailblazer::Operation.inherit(skip_operation.new: true)
4
+ # DISCUSS: don't instance_exec when |pipe| given?
5
+ operation.instance_exec(&block) # evaluate the nested pipe.
12
6
 
13
- class API
14
- include Pipetree::DSL
15
- include Pipetree::DSL::Macros
7
+ pipe = operation["pipetree"]
8
+ pipe.add(nil, nil, {delete: "operation.new"}) # TODO: make this a bit more elegant.
16
9
 
17
- def initialize(target, pipe)
18
- @target, @pipe = target, pipe
19
- end
10
+ step = Wrap.for(wrap, pipe)
20
11
 
21
- def _insert(operator, proc, options={}) # TODO: test me.
22
- Pipetree::DSL.insert(@pipe, operator, proc, options, definer_name: @target.name)
23
- end
12
+ [ step, {} ]
13
+ end
24
14
 
25
- def |(cfg, user_options={})
26
- Pipetree::DSL.import(@target, @pipe, cfg, user_options)
27
- end
28
- alias_method :step, :| # DISCUSS: uhm...
15
+ module Wrap
16
+ def self.for(wrap, pipe)
17
+ ->(input, options) { wrap.(options, input, pipe, & ->{ pipe.(input, options) }) }
29
18
  end
30
19
  end # Wrap
31
-
32
- DSL.macro!(:Wrap, Wrap)
33
20
  end
34
21
 
35
22
  # (options, *) => (options, operation, bla)
@@ -1,3 +1,3 @@
1
1
  module Trailblazer
2
- VERSION = "2.0.0"
2
+ VERSION = "2.0.1"
3
3
  end
@@ -19,10 +19,10 @@ class DryContainerTest < Minitest::Spec
19
19
  #- dependency injection
20
20
  #- with Dry-container
21
21
  class Create < Trailblazer::Operation
22
- self.| Model( Song, :new )
23
- self.| Contract::Build()
24
- self.| Contract::Validate()
25
- self.| Contract::Persist( method: :sync )
22
+ step Model( Song, :new )
23
+ step Contract::Build()
24
+ step Contract::Validate()
25
+ step Contract::Persist( method: :sync )
26
26
  end
27
27
  #:key end
28
28
 
@@ -0,0 +1,164 @@
1
+ require "test_helper"
2
+
3
+ class DocsFailFastOptionTest < Minitest::Spec
4
+ Song = Struct.new(:id, :title) do
5
+ def self.find_by(id); nil end
6
+ end
7
+
8
+ class MyContract < Reform::Form
9
+ end
10
+
11
+
12
+ #:ffopt
13
+ class Update < Trailblazer::Operation
14
+ step Model( Song, :find_by )
15
+ failure :abort!, fail_fast: true
16
+ step Contract::Build( constant: MyContract )
17
+ step Contract::Validate( )
18
+ failure :handle_invalid_contract! # won't be executed if #abort! is executed.
19
+
20
+ def abort!(options, params:, **)
21
+ options["result.model.song"] = "Something went wrong with ID #{params[:id]}!"
22
+ end
23
+ # ..
24
+ end
25
+ #:ffopt end
26
+
27
+ it { Update.(id: 1).inspect("result.model.song", "contract.default").must_equal %{<Result:false [\"Something went wrong with ID 1!\", nil] >} }
28
+ it do
29
+ #:ffopt-res
30
+ result = Update.(id: 1)
31
+ result["result.model.song"] #=> "Something went wrong with ID 1!"
32
+ #:ffopt-res end
33
+ end
34
+ end
35
+
36
+ class DocsFailFastOptionWithStepTest < Minitest::Spec
37
+ Song = Class.new do
38
+ def self.find_by(*); Object end
39
+ end
40
+
41
+ #:ffopt-step
42
+ class Update < Trailblazer::Operation
43
+ step :empty_id?, fail_fast: true
44
+ step Model( Song, :find_by )
45
+ failure :handle_empty_db! # won't be executed if #empty_id? returns falsey.
46
+
47
+ def empty_id?(options, params:, **)
48
+ params[:id] # returns false if :id missing.
49
+ end
50
+ end
51
+ #:ffopt-step end
52
+
53
+ it { Update.({ id: nil }).inspect("model").must_equal %{<Result:false [nil] >} }
54
+ it { Update.({ id: 1 }).inspect("model").must_equal %{<Result:true [Object] >} }
55
+ it do
56
+ #:ffopt-step-res
57
+ result = Update.({ id: nil })
58
+
59
+ result.failure? #=> true
60
+ result["model"] #=> nil
61
+ #:ffopt-step-res end
62
+ end
63
+ end
64
+
65
+ class DocsPassFastWithStepOptionTest < Minitest::Spec
66
+ Song = Struct.new(:id, :title) do
67
+ def self.find_by(id); nil end
68
+ end
69
+
70
+ class MyContract < Reform::Form
71
+ end
72
+
73
+ #:pfopt-step
74
+ class Update < Trailblazer::Operation
75
+ step Model( Song, :find_by )
76
+ failure :abort!, fail_fast: true
77
+ step Contract::Build( constant: MyContract )
78
+ step Contract::Validate( )
79
+ failure :handle_invalid_contract! # won't be executed if #abort! is executed.
80
+
81
+ def abort!(options, params:, **)
82
+ options["result.model.song"] = "Something went wrong with ID #{params[:id]}!"
83
+ end
84
+ # ..
85
+ end
86
+ #:pfopt-step end
87
+
88
+ it { Update.(id: 1).inspect("result.model.song", "contract.default").must_equal %{<Result:false [\"Something went wrong with ID 1!\", nil] >} }
89
+ it do
90
+ #:pfopt-step-res
91
+ result = Update.(id: 1)
92
+ result["result.model.song"] #=> "Something went wrong with ID 1!"
93
+ #:pfopt-step-res end
94
+ end
95
+ end
96
+
97
+ class DocsFailFastMethodTest < Minitest::Spec
98
+ Song = Struct.new(:id, :title) do
99
+ def self.find_by(id); nil end
100
+ end
101
+
102
+ #:ffmeth
103
+ class Update < Trailblazer::Operation
104
+ step :filter_params! # emits fail_fast!
105
+ step Model( Song, :find_by )
106
+ failure :handle_fail!
107
+
108
+ def filter_params!(options, params:, **)
109
+ unless params[:id]
110
+ options["result.params"] = "No ID in params!"
111
+ return Railway.fail_fast!
112
+ end
113
+ end
114
+
115
+ def handle_fail!(options, **)
116
+ options["my.status"] = "Broken!"
117
+ end
118
+ end
119
+ #:ffmeth end
120
+
121
+ it { Update.({}).inspect("result.params", "my.status").must_equal %{<Result:false [\"No ID in params!\", nil] >} }
122
+ it do
123
+ #:ffmeth-res
124
+ result = Update.(id: 1)
125
+ result["result.params"] #=> "No ID in params!"
126
+ result["my.status"] #=> nil
127
+ #:ffmeth-res end
128
+ end
129
+ end
130
+
131
+ class DocsPassFastMethodTest < Minitest::Spec
132
+ Song = Struct.new(:id, :title) do
133
+ def save; end
134
+ end
135
+
136
+ class MyContract < Reform::Form
137
+ end
138
+
139
+ #:pfmeth
140
+ class Create < Trailblazer::Operation
141
+ step Model( Song, :new )
142
+ step :empty_model! # emits pass_fast!
143
+ step Contract::Build( constant: MyContract )
144
+ # ..
145
+
146
+ def empty_model!(options, is_empty:, model:, **)
147
+ return unless is_empty
148
+ model.save
149
+ Railway.pass_fast!
150
+ end
151
+ end
152
+ #:pfmeth end
153
+
154
+ it { Create.({ title: "Tyrant"}, "is_empty" => true).inspect("model").must_equal %{<Result:true [#<struct DocsPassFastMethodTest::Song id=nil, title=nil>] >} }
155
+ it do
156
+ #:pfmeth-res
157
+ result = Create.({}, "is_empty" => true)
158
+ result["model"] #=> #<Song id=nil, title=nil>
159
+ #:pfmeth-res end
160
+ end
161
+ end
162
+
163
+ # fail!
164
+ # pass!