teckel 0.3.0 → 0.4.0

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -0
  3. data/LICENSE_LOGO +4 -0
  4. data/README.md +4 -4
  5. data/lib/teckel.rb +9 -3
  6. data/lib/teckel/chain.rb +99 -271
  7. data/lib/teckel/chain/result.rb +38 -0
  8. data/lib/teckel/chain/runner.rb +51 -0
  9. data/lib/teckel/chain/step.rb +18 -0
  10. data/lib/teckel/config.rb +1 -23
  11. data/lib/teckel/contracts.rb +19 -0
  12. data/lib/teckel/operation.rb +309 -215
  13. data/lib/teckel/operation/result.rb +92 -0
  14. data/lib/teckel/operation/runner.rb +70 -0
  15. data/lib/teckel/result.rb +52 -53
  16. data/lib/teckel/version.rb +1 -1
  17. data/spec/chain/inheritance_spec.rb +116 -0
  18. data/spec/chain/results_spec.rb +53 -0
  19. data/spec/chain_around_hook_spec.rb +100 -0
  20. data/spec/chain_spec.rb +180 -0
  21. data/spec/config_spec.rb +26 -0
  22. data/spec/doctest_helper.rb +7 -0
  23. data/spec/operation/inheritance_spec.rb +94 -0
  24. data/spec/operation/result_spec.rb +34 -0
  25. data/spec/operation/results_spec.rb +117 -0
  26. data/spec/operation_spec.rb +485 -0
  27. data/spec/rb27/pattern_matching_spec.rb +193 -0
  28. data/spec/result_spec.rb +20 -0
  29. data/spec/spec_helper.rb +25 -0
  30. data/spec/support/dry_base.rb +8 -0
  31. data/spec/support/fake_db.rb +12 -0
  32. data/spec/support/fake_models.rb +20 -0
  33. data/spec/teckel_spec.rb +7 -0
  34. metadata +52 -25
  35. data/.codeclimate.yml +0 -3
  36. data/.github/workflows/ci.yml +0 -92
  37. data/.github/workflows/pages.yml +0 -50
  38. data/.gitignore +0 -15
  39. data/.rspec +0 -3
  40. data/.rubocop.yml +0 -12
  41. data/.ruby-version +0 -1
  42. data/DEVELOPMENT.md +0 -32
  43. data/Gemfile +0 -16
  44. data/Rakefile +0 -35
  45. data/bin/console +0 -15
  46. data/bin/rake +0 -29
  47. data/bin/rspec +0 -29
  48. data/bin/rubocop +0 -18
  49. data/bin/setup +0 -8
  50. data/lib/teckel/none.rb +0 -18
  51. data/lib/teckel/operation/results.rb +0 -72
  52. data/teckel.gemspec +0 -32
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 520b717d1e115614b1c93cfac56deac0e567bfc5d39e2ca6e93094f3c958fd9b
4
- data.tar.gz: d5f8f5a5dd2a2dabebe1add49ab139e9c5d14fd5c1fd3b1ef92f78c99c604401
3
+ metadata.gz: 18179b7fd315a11bc51f0ac4cfbdba4c0e74ad82a6a7dc186289433906ec2783
4
+ data.tar.gz: 02eb5f5c59ac9983a92b457f9253a38f9188e3f2b17d4f30029aa545b3151181
5
5
  SHA512:
6
- metadata.gz: cb4021c0102a0d18e45df23a40669984462b5d469608f16cefcff68fefa38a088ae4851f51052ba5b3c3f5305dc6d0abfbf2a50a81ca240748cf226ea5b66366
7
- data.tar.gz: 4298d3bff376cace7dd78a863fc60da2f7e4600f8032da19a3c7ecaee6168146fe41658af16e1811ad7de157cf15a37aa0bad42cdc19602de8eac1b2a2f49a83
6
+ metadata.gz: cbbbebfecccf6806da642dbc1dce843946c7dcaa27abb7c38467b0512ff5cb32d13b848fae0eaa0f8c4641f26e5b1f03f58f82fb70b3aa8aa1a891356698ff16
7
+ data.tar.gz: b312534680f31d7cff2a13326f3dc3cc01566110d3001b285d233c97548edbfb2fe008d59821ef0723b63687f12e95cca3f06f2f7c1a3f8d0e15d90caca4733d
@@ -1,5 +1,58 @@
1
1
  # Changes
2
2
 
3
+ ## 0.4.0
4
+
5
+ - Moving verbose examples from API docs into github pages
6
+ - `#finalize!` no longer freezes the entire Operation or Chain class, only it's settings. [GH-13]
7
+ - Add simple support for using Base classes. [GH-10]
8
+ Removes global configuration `Teckel::Config.default_constructor`
9
+ ```ruby
10
+ class ApplicationOperation
11
+ include Teckel::Operation
12
+ # you won't be able to overwrite any configuration in child classes,
13
+ # so take care which you want to declare
14
+ result!
15
+ settings Struct.new(:logger)
16
+ input_constructor :new
17
+ error Struct.new(:status, :messages)
18
+
19
+ def log(message)
20
+ return unless settings&.logger
21
+ logger << message
22
+ end
23
+ # you cannot call `finalize!` on partially declared Operations
24
+ end
25
+ ```
26
+ - Add support for setting your own Result objects. [GH-9]
27
+ - They should include and implement `Teckel::Result` which is needed by `Chain`.
28
+ - `Chain::StepFailure` got replaced with `Chain::Result`.
29
+ - the `Teckel::Operation::Results` module was removed. To let Operation use the default Result object, use the new helper `result!` instead.
30
+ - Add "settings"/dependency injection to Operation and Chains. [GH-7]
31
+ ```ruby
32
+ MyOperation.with(logger: STDOUT).call(params)
33
+
34
+ MyChain.with(some_step: { logger: STDOUT }).call(params)
35
+ ```
36
+ - [GH-5] Add support for ruby 2.7 pattern matching on Operation and Chain results. Both, array and hash notations are supported:
37
+ ```ruby
38
+ case MyOperation.call(params)
39
+ in [false, value]
40
+ # handle failure
41
+ in [true, value]
42
+ # handle success
43
+ end
44
+
45
+ case MyChain.call(params)
46
+ in { success: false, step: :foo, value: value }
47
+ # handle foo failure
48
+ in [success: false, step: :bar, value: value }
49
+ # handle bar failure
50
+ in { success: true, value: value }
51
+ # handle success
52
+ end
53
+ ```
54
+ - Fix setting a config twice to raise an error
55
+
3
56
  ## 0.3.0
4
57
 
5
58
  - `finalize!`'ing a Chain will also finalize all it's Operations
@@ -0,0 +1,4 @@
1
+ The Teckel Logo artwork is NOT part of the Work as described in the LICENSE.
2
+ Any Derivative Works shall not use the Teckel Logo.
3
+
4
+ Copyright 2020 Jana Vogel <jana@dotless.de>
data/README.md CHANGED
@@ -34,11 +34,11 @@ Working with [Interactor](https://github.com/collectiveidea/interactor), [Trailb
34
34
 
35
35
  ## Usage
36
36
 
37
- For a full overview please see the Api Docs:
37
+ For a full overview please see the Docs:
38
38
 
39
- * [Operations](https://fnordfish.github.io/teckel/doc/Teckel/Operation.html)
40
- * [Operations with Result objects](https://fnordfish.github.io/teckel/doc/Teckel/Operation/Results.html)
41
- * [Chains](https://fnordfish.github.io/teckel/doc/Teckel/Chain.html)
39
+ * [Operations](https://fnordfish.github.io/teckel/operations/basics/)
40
+ * [Result Objects](https://fnordfish.github.io/teckel/operations/result_objects/)
41
+ * [Chains](https://fnordfish.github.io/teckel/chains/basics/)
42
42
 
43
43
 
44
44
  ```ruby
@@ -3,14 +3,20 @@
3
3
  require "teckel/version"
4
4
 
5
5
  module Teckel
6
+ # Base error class for this lib
6
7
  class Error < StandardError; end
8
+
9
+ # configuring the same value twice will raise this
7
10
  class FrozenConfigError < Teckel::Error; end
11
+
12
+ # missing important configurations (like contracts) will raise this
8
13
  class MissingConfigError < Teckel::Error; end
14
+
15
+ DEFAULT_CONSTRUCTOR = :[]
9
16
  end
10
17
 
11
18
  require_relative "teckel/config"
12
- require_relative "teckel/none"
13
- require_relative "teckel/operation"
19
+ require_relative "teckel/contracts"
14
20
  require_relative "teckel/result"
15
- require_relative "teckel/operation/results"
21
+ require_relative "teckel/operation"
16
22
  require_relative "teckel/chain"
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'forwardable'
3
+ require_relative 'chain/step'
4
+ require_relative 'chain/result'
5
+ require_relative 'chain/runner'
4
6
 
5
7
  module Teckel
6
8
  # Railway style execution of multiple Operations.
@@ -8,253 +10,11 @@ module Teckel
8
10
  # - Runs multiple Operations (steps) in order.
9
11
  # - The output of an earlier step is passed as input to the next step.
10
12
  # - Any failure will stop the execution chain (none of the later steps is called).
11
- # - All Operations (steps) must behave like
12
- # {Teckel::Operation::Results Teckel::Operation::Results} and return a result
13
- # object like {Teckel::Result}
14
- # - A failure response is wrapped into a {Teckel::Chain::StepFailure} giving
15
- # additional information about which step failed
13
+ # - All Operations (steps) must return a {Teckel::Result}
14
+ # - The result is wrapped into a {Teckel::Chain::Result}
16
15
  #
17
- # @see Teckel::Operation::Results
18
- #
19
- # @example Defining a simple Chain with three steps
20
- # class CreateUser
21
- # include ::Teckel::Operation::Results
22
- #
23
- # input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer.optional)
24
- # output Types.Instance(User)
25
- # error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
26
- #
27
- # def call(input)
28
- # user = User.new(name: input[:name], age: input[:age])
29
- # if user.save
30
- # success!(user)
31
- # else
32
- # fail!(message: "Could not save User", errors: user.errors)
33
- # end
34
- # end
35
- # end
36
- #
37
- # class LogUser
38
- # include ::Teckel::Operation::Results
39
- #
40
- # input Types.Instance(User)
41
- # output input
42
- #
43
- # def call(usr)
44
- # Logger.new(File::NULL).info("User #{usr.name} created")
45
- # usr # we need to return the correct output type
46
- # end
47
- # end
48
- #
49
- # class AddFriend
50
- # class << self
51
- # # Don't actually do this! It's not safe and for generating the failure sample only.
52
- # attr_accessor :fail_befriend
53
- # end
54
- #
55
- # include ::Teckel::Operation::Results
56
- #
57
- # input Types.Instance(User)
58
- # output Types::Hash.schema(user: Types.Instance(User), friend: Types.Instance(User))
59
- # error Types::Hash.schema(message: Types::String)
60
- #
61
- # def call(user)
62
- # if self.class.fail_befriend
63
- # fail!(message: "Did not find a friend.")
64
- # else
65
- # { user: user, friend: User.new(name: "A friend", age: 42) }
66
- # end
67
- # end
68
- # end
69
- #
70
- # class MyChain
71
- # include Teckel::Chain
72
- #
73
- # step :create, CreateUser
74
- # step :log, LogUser
75
- # step :befriend, AddFriend
76
- # end
77
- #
78
- # result = MyChain.call(name: "Bob", age: 23)
79
- # result.is_a?(Teckel::Result) #=> true
80
- # result.success[:user].is_a?(User) #=> true
81
- # result.success[:friend].is_a?(User) #=> true
82
- #
83
- # AddFriend.fail_befriend = true
84
- # failure_result = MyChain.call(name: "Bob", age: 23)
85
- # failure_result.is_a?(Teckel::Chain::StepFailure) #=> true
86
- #
87
- # # additional step information
88
- # failure_result.step #=> :befriend
89
- # failure_result.operation #=> AddFriend
90
- #
91
- # # otherwise behaves just like a normal +Result+
92
- # failure_result.failure? #=> true
93
- # failure_result.failure #=> {message: "Did not find a friend."}
94
- #
95
- # @example DB transaction around hook
96
- # class CreateUser
97
- # include ::Teckel::Operation::Results
98
- #
99
- # input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer.optional)
100
- # output Types.Instance(User)
101
- # error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
102
- #
103
- # def call(input)
104
- # user = User.new(name: input[:name], age: input[:age])
105
- # if user.save
106
- # success!(user)
107
- # else
108
- # fail!(message: "Could not safe User", errors: user.errors)
109
- # end
110
- # end
111
- # end
112
- #
113
- # class AddFriend
114
- # class << self
115
- # # Don't actually do this! It's not safe and for generating the failure sample only.
116
- # attr_accessor :fail_befriend
117
- # end
118
- #
119
- # include ::Teckel::Operation::Results
120
- #
121
- # input Types.Instance(User)
122
- # output Types::Hash.schema(user: Types.Instance(User), friend: Types.Instance(User))
123
- # error Types::Hash.schema(message: Types::String)
124
- #
125
- # def call(user)
126
- # if self.class.fail_befriend
127
- # fail!(message: "Did not find a friend.")
128
- # else
129
- # { user: user, friend: User.new(name: "A friend", age: 42) }
130
- # end
131
- # end
132
- # end
133
- #
134
- # LOG = []
135
- #
136
- # class MyChain
137
- # include Teckel::Chain
138
- #
139
- # around ->(chain, input) {
140
- # result = nil
141
- # begin
142
- # LOG << :before
143
- #
144
- # FakeDB.transaction do
145
- # result = chain.call(input)
146
- # raise FakeDB::Rollback if result.failure?
147
- # end
148
- #
149
- # LOG << :after
150
- # result
151
- # rescue FakeDB::Rollback
152
- # LOG << :rollback
153
- # result
154
- # end
155
- # }
156
- #
157
- # step :create, CreateUser
158
- # step :befriend, AddFriend
159
- # end
160
- #
161
- # AddFriend.fail_befriend = true
162
- # failure_result = MyChain.call(name: "Bob", age: 23)
163
- # failure_result.is_a?(Teckel::Chain::StepFailure) #=> true
164
- #
165
- # # triggered DB rollback
166
- # LOG #=> [:before, :rollback]
167
- #
168
- # # additional step information
169
- # failure_result.step #=> :befriend
170
- # failure_result.operation #=> AddFriend
171
- #
172
- # # otherwise behaves just like a normal +Result+
173
- # failure_result.failure? #=> true
174
- # failure_result.failure #=> {message: "Did not find a friend."}
16
+ # @see Teckel::Operation#result!
175
17
  module Chain
176
- # Internal wrapper of a step definition
177
- Step = Struct.new(:name, :operation) do
178
- def finalize!
179
- name.freeze
180
- operation.finalize!
181
- freeze
182
- end
183
- end
184
-
185
- # Like {Teckel::Result Teckel::Result} but for failing Chains
186
- #
187
- # When a Chain fails, it stores the failed +Operation+ and it's name.
188
- class StepFailure
189
- extend Forwardable
190
-
191
- def initialize(step, result)
192
- @step, @result = step, result
193
- end
194
-
195
- # @!method step
196
- # Delegates to +step.name+
197
- # @return [String,Symbol] The name of the failed operation.
198
- def_delegator :@step, :name, :step
199
-
200
- # @!method operation
201
- # Delegates to +step.operation+
202
- # @return [Teckel::Operation] The failed Operation class.
203
- def_delegator :@step, :operation
204
-
205
- # @!attribute result [R]
206
- # @return [Teckel::Result] the failure Result
207
- attr_reader :result
208
-
209
- # @!method value
210
- # Delegates to +result.value+
211
- # @see Teckel::Result#value
212
- # @!method successful?
213
- # Delegates to +result.successful?+
214
- # @see Teckel::Result#successful?
215
- # @!method success
216
- # Delegates to +result.success+
217
- # @see Teckel::Result#success
218
- # @!method failure?
219
- # Delegates to +result.failure?+
220
- # @see Teckel::Result#failure?
221
- # @!method failure
222
- # Delegates to +result.failure+
223
- # @see Teckel::Result#failure
224
- def_delegators :@result, :value, :successful?, :success, :failure?, :failure
225
- end
226
-
227
- # The default implementation for executing a {Chain}
228
- #
229
- # @!visibility protected
230
- class Runner
231
- def initialize(steps)
232
- @steps = steps
233
- end
234
- attr_reader :steps
235
-
236
- # Run steps
237
- #
238
- # @param input Any form of input the first steps +input+ class can handle
239
- #
240
- # @return [Teckel::Result,Teckel::Chain::StepFailure] The result object wrapping
241
- # either the success or failure value. Note that the {StepFailure} behaves
242
- # just like a {Teckel::Result} with added information about which step failed.
243
- def call(input)
244
- last_result = input
245
- failed = nil
246
- steps.each do |step|
247
- last_result = step.operation.call(last_result)
248
- if last_result.failure?
249
- failed = StepFailure.new(step, last_result)
250
- break
251
- end
252
- end
253
-
254
- failed || last_result
255
- end
256
- end
257
-
258
18
  module ClassMethods
259
19
  # The expected input for this chain
260
20
  # @return [Class] The {Teckel::Operation.input} of the first step
@@ -281,8 +41,8 @@ module Teckel
281
41
  #
282
42
  # @param name [String,Symbol] The name of the operation.
283
43
  # This name is used in an error case to let you know which step failed.
284
- # @param operation [Operation::Results] The operation to call.
285
- # Must return a {Teckel::Result} object.
44
+ # @param operation [Operation] The operation to call, which
45
+ # must return a {Teckel::Result} object.
286
46
  def step(name, operation)
287
47
  steps << Step.new(name, operation)
288
48
  end
@@ -308,7 +68,8 @@ module Teckel
308
68
  # OUTPUTS = []
309
69
  #
310
70
  # class Echo
311
- # include ::Teckel::Operation::Results
71
+ # include ::Teckel::Operation
72
+ # result!
312
73
  #
313
74
  # input Hash
314
75
  # output input
@@ -349,15 +110,57 @@ module Teckel
349
110
  @config.for(:runner, klass) { Runner }
350
111
  end
351
112
 
113
+ # @overload result()
114
+ # Get the configured result object class wrapping {.error} or {.output}.
115
+ # @return [Class] The +result+ class, or {Teckel::Chain::Result} as default
116
+ #
117
+ # @overload result(klass)
118
+ # Set the result object class wrapping {.error} or {.output}.
119
+ # @param klass [Class] The +result+ class
120
+ # @return [Class] The +result+ class configured
121
+ def result(klass = nil)
122
+ @config.for(:result, klass) { const_defined?(:Result, false) ? self::Result : Teckel::Chain::Result }
123
+ end
124
+
125
+ # @overload result_constructor()
126
+ # The callable constructor to build an instance of the +result+ class.
127
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
128
+ # @return [Proc] A callable that will return an instance of +result+ class.
129
+ #
130
+ # @overload result_constructor(sym_or_proc)
131
+ # Define how to build the +result+.
132
+ # @param sym_or_proc [Symbol, #call]
133
+ # - Either a +Symbol+ representing the _public_ method to call on the +result+ class.
134
+ # - Or anything that response to +#call+ (like a +Proc+).
135
+ # @return [#call] The callable constructor
136
+ #
137
+ # @example
138
+ # class MyOperation
139
+ # include Teckel::Operation
140
+ #
141
+ # class Result < Teckel::Operation::Result
142
+ # def initialize(value, success, step, options = {}); end
143
+ # end
144
+ #
145
+ # # If you need more control over how to build a new +Settings+ instance
146
+ # result_constructor ->(value, success, step) { result.new(value, success, step, {foo: :bar}) }
147
+ # end
148
+ def result_constructor(sym_or_proc = nil)
149
+ constructor = build_counstructor(result, sym_or_proc) unless sym_or_proc.nil?
150
+
151
+ @config.for(:result_constructor, constructor) {
152
+ build_counstructor(result, Teckel::DEFAULT_CONSTRUCTOR)
153
+ } || raise(MissingConfigError, "Missing result_constructor config for #{self}")
154
+ end
155
+
352
156
  # The primary interface to call the chain with the given input.
353
157
  #
354
158
  # @param input Any form of input the first steps +input+ class can handle
355
159
  #
356
- # @return [Teckel::Result,Teckel::Chain::StepFailure] The result object wrapping
357
- # either the success or failure value. Note that the {StepFailure} behaves
358
- # just like a {Teckel::Result} with added information about which step failed.
359
- def call(input)
360
- runner = self.runner.new(steps)
160
+ # @return [Teckel::Chain::Result] The result object wrapping
161
+ # the result value, the success state and last executed step.
162
+ def call(input = nil)
163
+ runner = self.runner.new(self)
361
164
  if around
362
165
  around.call(runner, input)
363
166
  else
@@ -365,12 +168,23 @@ module Teckel
365
168
  end
366
169
  end
367
170
 
171
+ # @param settings [Hash{String,Symbol => Object}] Set settings for a step by it's name
172
+ def with(settings)
173
+ runner = self.runner.new(self, settings)
174
+ if around
175
+ ->(input) { around.call(runner, input) }
176
+ else
177
+ runner
178
+ end
179
+ end
180
+ alias :set :with
181
+
368
182
  # @!visibility private
369
183
  # @return [void]
370
184
  def define!
371
185
  raise MissingConfigError, "Cannot define Chain with no steps" if steps.empty?
372
186
 
373
- %i[around runner].each { |e| public_send(e) }
187
+ %i[around runner result result_constructor].each { |e| public_send(e) }
374
188
  steps.each(&:finalize!)
375
189
  nil
376
190
  end
@@ -384,7 +198,7 @@ module Teckel
384
198
  define!
385
199
  steps.freeze
386
200
  @config.freeze
387
- freeze
201
+ self
388
202
  end
389
203
 
390
204
  # Produces a shallow copy of this chain.
@@ -393,12 +207,7 @@ module Teckel
393
207
  # @return [self]
394
208
  # @!visibility public
395
209
  def dup
396
- super.tap do |copy|
397
- new_config = @config.dup
398
- new_config.replace(:steps) { steps.dup }
399
-
400
- copy.instance_variable_set(:@config, new_config)
401
- end
210
+ dup_config(super)
402
211
  end
403
212
 
404
213
  # Produces a clone of this chain.
@@ -410,22 +219,41 @@ module Teckel
410
219
  if frozen?
411
220
  super
412
221
  else
413
- super.tap do |copy|
414
- new_config = @config.dup
415
- new_config.replace(:steps) { steps.dup }
222
+ dup_config(super)
223
+ end
224
+ end
416
225
 
417
- copy.instance_variable_set(:@config, new_config)
418
- end
226
+ # @!visibility private
227
+ def inherited(subclass)
228
+ dup_config(subclass)
229
+ end
230
+
231
+ # @!visibility private
232
+ def self.extended(base)
233
+ base.instance_variable_set(:@config, Config.new)
234
+ end
235
+
236
+ private
237
+
238
+ def dup_config(other_class)
239
+ new_config = @config.dup
240
+ new_config.replace(:steps) { steps.dup }
241
+
242
+ other_class.instance_variable_set(:@config, new_config)
243
+ other_class
244
+ end
245
+
246
+ def build_counstructor(on, sym_or_proc)
247
+ if sym_or_proc.is_a?(Symbol) && on.respond_to?(sym_or_proc)
248
+ on.public_method(sym_or_proc)
249
+ elsif sym_or_proc.respond_to?(:call)
250
+ sym_or_proc
419
251
  end
420
252
  end
421
253
  end
422
254
 
423
255
  def self.included(receiver)
424
256
  receiver.extend ClassMethods
425
-
426
- receiver.class_eval do
427
- @config = Config.new
428
- end
429
257
  end
430
258
  end
431
259
  end