trailblazer 2.0.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
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!