teckel 0.7.0 → 0.9.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df9c73577c9c93fbaaf2e7661a07bdc3b1b0397832a62a8edbe4eea846c4c029
4
- data.tar.gz: 037041bf0eb6ae8bc41e354772d3ce1d04002a35ecccc0b1ecefa33a85845c3b
3
+ metadata.gz: c8dc1958f2645a6c57178a595b7d4364c9b2613e3c5adc532e0c8fd0cc8c2c22
4
+ data.tar.gz: cc0ef87983b17af9c16df5f4cc77a826a445fb7dc1f38db4de6cef7eef3b27c1
5
5
  SHA512:
6
- metadata.gz: 2f6f601e816c972aed8e511b05fdbc418a48aab10c2ca246d108661cc25a8d514a41944b2e01b9294757a2a717696491f20f601f5f60e53faa45651aec48bc7f
7
- data.tar.gz: 68bc46e5ed21cf5e639ee1fc00265b278f9062f42de9d179a07559862990a078df9dcb2ef26956eb629cf08e71409b9b05bd363a11930cb64ecdee533d63d2ea
6
+ metadata.gz: 9b9e5dde43b07ec1ae8fb6a1964990cdbdac430bd2c3ba4e73fd137fbeac3f1e4a3faf10e05098da00414a8ed17b6b8ce4316593ea1160bf976aeb2bc550ab6d
7
+ data.tar.gz: 5fd5bd9a76fcf3ad4794de79e94eaba128dc6cbcac1a12e92eafa0aef36d5b18496d8d509738706f23fe555e6d482c21e20cd0384c756e4b02952d4ce52bf770
data/CHANGELOG.md CHANGED
@@ -1,24 +1,58 @@
1
1
  # Changes
2
2
 
3
+ ## 0.9.0
4
+
5
+ - Internal housekeeping
6
+ - Add ruby 3.4 and 'head' to CI
7
+ - Ruby < 3 won't run mutation test
8
+ - moved to standard.rb for linting and formatting
9
+ - removed usage of `forwardable` in favor of explicit delegation methods
10
+ - renamed internal `Teckel::Config.for` to `get_or_set`
11
+ - renamed internal `Teckel::Operation::Config.get_set_constructor` to `get_or_set_constructor`
12
+ - fixed specs that test backtrace outputs on ruby 3.4
13
+ - Documentation now uses modern (Ruby 3.4) output syntax. (`{foo: 1}` is now printed as is, instead of `{:foo => 1}`
14
+
15
+ ## 0.8.0
16
+
17
+ - Add mutation testing (currently about 80% covered)
18
+ - Breaking: `Teckel::Operation::Result` no longer converts the `successful` value to a boolean.
19
+ When using the default result implementation, nothing changes for you.
20
+ When you manually pass anything else into it, `succesful?` will return this value. The `failure` and `success` methods
21
+ work on the "Truthy" value of `successful`
22
+
23
+ ```ruby
24
+ result = Result.new(some_value, 42)
25
+ result.successful?
26
+ # => 42
27
+ ```
28
+
29
+ - Change: `freeze`ing an `Operation` or `Chain` will also freeze their internal config (input, output, steps, etc.)
30
+
31
+ Internal:
32
+
33
+ - Some refactoring to cover mutations
34
+ - Extracted creating the runable `Operation` instance into a new, public `runable(settings = UNDEFINED)` method.
35
+ Mainly to reduce code duplication but this also makes testing, stubbing and mocking easier.
36
+
3
37
  ## 0.7.0
4
38
 
5
39
  - Breaking: `Teckel::Chain` will not be required by default. require manually if needed `require "teckel/chain"` [GH-24]
6
40
  - Breaking: Internally, `Teckel::Operation::Runner` instead of `:success` and `:failure` now only uses `:halt` as it's throw-catch symbol. [GH-26]
7
41
  - Add: Using the default `Teckel::Operation::Runner`, `input_constructor` and `result_constructor` will be executed
8
- within the context of the operation instance. This allows for `input_constructor` to call `fail!` and `success!`
42
+ within the context of the operation instance. This allows for `input_constructor` to call `fail!` and `success!`
9
43
  without ever `call`ing the operation. [GH-26]
10
44
 
11
-
12
45
  ## 0.6.0
13
46
 
14
47
  - Breaking: Operations return values will be ignored. [GH-21]
15
- * You'll need to use `success!` or `failure!`
16
- * `success!` and `failure!` are now implemented on the `Runner`, which makes it easier to change their behavior (including the one above).
48
+ - You'll need to use `success!` or `failure!`
49
+ - `success!` and `failure!` are now implemented on the `Runner`, which makes it easier to change their behavior (including the one above).
17
50
 
18
51
  ## 0.5.0
19
52
 
20
53
  - Fix: calling chain with settings and no input [GH-14]
21
54
  - Add: Default settings for Operation and Chains [GH-17], [GH-18]
55
+
22
56
  ```ruby
23
57
  class MyOperation
24
58
  include Teckel::Operation
@@ -48,6 +82,7 @@
48
82
  ```
49
83
 
50
84
  Internal:
85
+
51
86
  - Move operation and chain config dsl methods into own module [GH-15]
52
87
  - Code simplifications [GH-16]
53
88
 
@@ -57,6 +92,7 @@ Internal:
57
92
  - `#finalize!` no longer freezes the entire Operation or Chain class, only it's settings. [GH-13]
58
93
  - Add simple support for using Base classes. [GH-10]
59
94
  Removes global configuration `Teckel::Config.default_constructor`
95
+
60
96
  ```ruby
61
97
  class ApplicationOperation
62
98
  include Teckel::Operation
@@ -74,17 +110,21 @@ Internal:
74
110
  # you cannot call `finalize!` on partially declared Operations
75
111
  end
76
112
  ```
113
+
77
114
  - Add support for setting your own Result objects. [GH-9]
78
115
  - They should include and implement `Teckel::Result` which is needed by `Chain`.
79
116
  - `Chain::StepFailure` got replaced with `Chain::Result`.
80
117
  - the `Teckel::Operation::Results` module was removed. To let Operation use the default Result object, use the new helper `result!` instead.
81
118
  - Add "settings"/dependency injection to Operation and Chains. [GH-7]
119
+
82
120
  ```ruby
83
121
  MyOperation.with(logger: STDOUT).call(params)
84
122
 
85
123
  MyChain.with(some_step: { logger: STDOUT }).call(params)
86
124
  ```
125
+
87
126
  - [GH-5] Add support for ruby 2.7 pattern matching on Operation and Chain results. Both, array and hash notations are supported:
127
+
88
128
  ```ruby
89
129
  case MyOperation.call(params)
90
130
  in [false, value]
@@ -102,19 +142,20 @@ Internal:
102
142
  # handle success
103
143
  end
104
144
  ```
145
+
105
146
  - Fix setting a config twice to raise an error
106
147
 
107
148
  ## 0.3.0
108
149
 
109
150
  - `finalize!`'ing a Chain will also finalize all it's Operations
110
151
  - Changed attribute naming of `StepFailure`:
111
- + `.operation` will now give the operation class of the step - was `.step` before
112
- + `.step` will now give the name of the step (which Operation failed) - was `.step_name` before
152
+ - `.operation` will now give the operation class of the step - was `.step` before
153
+ - `.step` will now give the name of the step (which Operation failed) - was `.step_name` before
113
154
 
114
155
  ## 0.2.0
115
156
 
116
157
  - Around Hooks for Chains
117
- - `finalize!`
158
+ - `finalize!`
118
159
  - freezing Chains and Operations, to prevent further changes
119
160
  - Operations check their config and raise if any is missing
120
161
 
data/README.md CHANGED
@@ -3,10 +3,10 @@
3
3
  Ruby service classes with enforced<sup name="footnote-1-source">[1](#footnote-1)</sup> input, output and error data structure definition.
4
4
 
5
5
  [![Gem Version](https://img.shields.io/gem/v/teckel.svg)][gem]
6
- [![Build Status](https://github.com/dry-rb/dry-configurable/workflows/ci/badge.svg)][ci]
6
+ [![Build Status](https://github.com/fnordfish/teckel/actions/workflows/specs.yml/badge.svg)][ci]
7
7
  [![Maintainability](https://api.codeclimate.com/v1/badges/b3939aaec6271a567a57/maintainability)](https://codeclimate.com/github/fnordfish/teckel/maintainability)
8
8
  [![Test Coverage](https://api.codeclimate.com/v1/badges/b3939aaec6271a567a57/test_coverage)](https://codeclimate.com/github/fnordfish/teckel/test_coverage)
9
- [![API Documentation Coverage](https://inch-ci.org/github/fnordfish/teckel.svg?branch=master)][inch]
9
+ [![API Documentation Coverage](https://inch-ci.org/github/fnordfish/teckel.svg?branch=main)][inch]
10
10
 
11
11
  ## Installation
12
12
 
@@ -95,5 +95,5 @@ Please also see [DEVELOPMENT.md](DEVELOPMENT.md) for planned features and genera
95
95
  - <a name="footnote-1">1</a>: Obviously, it's still Ruby and you can cheat. Don’t! [↩](#footnote-1-source)
96
96
 
97
97
  [gem]: https://rubygems.org/gems/teckel
98
- [ci]: https://github.com/fnordfish/teckel/actions?query=workflow%3ACI
98
+ [ci]: https://github.com/fnordfish/teckel/actions/workflows/specs.yml
99
99
  [inch]: http://inch-ci.org/github/fnordfish/teckel
@@ -17,7 +17,7 @@ module Teckel
17
17
  #
18
18
  # @return [<Step>]
19
19
  def steps
20
- @config.for(:steps) { [] }
20
+ @config.get_or_set(:steps) { [] }
21
21
  end
22
22
 
23
23
  # Set or get the optional around hook.
@@ -26,9 +26,9 @@ module Teckel
26
26
  # chain ({Runner}) and the second argument the +input+ data. The hook also
27
27
  # needs to return the result.
28
28
  #
29
- # @param callable [Proc,{#call}] The hook to pass chain execution control to. (nil)
29
+ # @param callable [Proc,#call] The hook to pass chain execution control to. (nil)
30
30
  #
31
- # @return [Proc,{#call}] The configured hook
31
+ # @return [Proc,#call] The configured hook
32
32
  #
33
33
  # @example Around hook with block
34
34
  # OUTPUTS = []
@@ -62,7 +62,7 @@ module Teckel
62
62
  # OUTPUTS #=> ["before start", "after start"]
63
63
  # result.success #=> { some: "test" }
64
64
  def around(callable = nil, &block)
65
- @config.for(:around, callable || block)
65
+ @config.get_or_set(:around, callable || block)
66
66
  end
67
67
 
68
68
  # @!attribute [r] runner()
@@ -73,7 +73,7 @@ module Teckel
73
73
  # @param klass [Class] A class like the {Runner}
74
74
  # @!visibility protected
75
75
  def runner(klass = nil)
76
- @config.for(:runner, klass) { Runner }
76
+ @config.get_or_set(:runner, klass) { Runner }
77
77
  end
78
78
 
79
79
  # @overload result()
@@ -85,7 +85,7 @@ module Teckel
85
85
  # @param klass [Class] The +result+ class
86
86
  # @return [Class] The +result+ class configured
87
87
  def result(klass = nil)
88
- @config.for(:result, klass) { const_defined?(:Result, false) ? self::Result : Teckel::Chain::Result }
88
+ @config.get_or_set(:result, klass) { const_defined?(:Result, false) ? self::Result : Teckel::Chain::Result }
89
89
  end
90
90
 
91
91
  # @overload result_constructor()
@@ -143,17 +143,17 @@ module Teckel
143
143
  def result_constructor(sym_or_proc = nil)
144
144
  constructor = build_constructor(result, sym_or_proc) unless sym_or_proc.nil?
145
145
 
146
- @config.for(:result_constructor, constructor) {
146
+ @config.get_or_set(:result_constructor, constructor) {
147
147
  build_constructor(result, Teckel::DEFAULT_CONSTRUCTOR)
148
148
  } || raise(MissingConfigError, "Missing result_constructor config for #{self}")
149
149
  end
150
150
 
151
- # Declare default settings operation iin this chain should use when called without
151
+ # Declare default settings operation in this chain should use when called without
152
152
  # {Teckel::Chain::ClassMethods#with #with}.
153
153
  #
154
154
  # Explicit call-time settings will *not* get merged with declared default setting.
155
155
  #
156
- # @param settings [Hash{String,Symbol => Object}] Set settings for a step by it's name
156
+ # @param settings [Hash{(String,Symbol) => Object}] Set settings for a step by it's name
157
157
  #
158
158
  # @example
159
159
  # class MyOperation
@@ -188,15 +188,18 @@ module Teckel
188
188
  # result = Chain.with(a: { other: "What" }).call
189
189
  # result.success #=> {say: nil, other: "What"}
190
190
  def default_settings!(settings) # :nodoc: The bang is for consistency with the Operation class
191
- @config.for(:default_settings, settings)
191
+ @config.get_or_set(:default_settings, settings)
192
192
  end
193
193
 
194
194
  # Getter for configured default settings
195
- # @return [nil|#call] The callable constructor
195
+ # @return [NilClass]
196
+ # @return [#call] The callable constructor
196
197
  def default_settings
197
- @config.for(:default_settings)
198
+ @config.get_or_set(:default_settings)
198
199
  end
199
200
 
201
+ # @!visibility private
202
+ # @return [Array<Symbol>]
200
203
  REQUIRED_CONFIGS = %i[around runner result result_constructor].freeze
201
204
 
202
205
  # @!visibility private
@@ -227,7 +230,7 @@ module Teckel
227
230
  # @return [self]
228
231
  # @!visibility public
229
232
  def dup
230
- dup_config(super)
233
+ dup_config(super()) # standard:disable Style/SuperArguments
231
234
  end
232
235
 
233
236
  # Produces a clone of this chain.
@@ -237,15 +240,25 @@ module Teckel
237
240
  # @!visibility public
238
241
  def clone
239
242
  if frozen?
240
- super
243
+ super() # standard:disable Style/SuperArguments
241
244
  else
242
- dup_config(super)
245
+ dup_config(super()) # standard:disable Style/SuperArguments
243
246
  end
244
247
  end
245
248
 
249
+ # Prevents further modifications to this chain and its config
250
+ #
251
+ # @return [self]
252
+ # @!visibility public
253
+ def freeze
254
+ steps.freeze
255
+ @config.freeze
256
+ super() # standard:disable Style/SuperArguments
257
+ end
258
+
246
259
  # @!visibility private
247
260
  def inherited(subclass)
248
- super dup_config(subclass)
261
+ super(dup_config(subclass))
249
262
  end
250
263
 
251
264
  # @!visibility private
@@ -253,9 +266,7 @@ module Teckel
253
266
  base.instance_variable_set(:@config, Teckel::Config.new)
254
267
  end
255
268
 
256
- private
257
-
258
- def dup_config(other_class)
269
+ private def dup_config(other_class)
259
270
  new_config = @config.dup
260
271
  new_config.replace(:steps) { steps.dup }
261
272
 
@@ -263,11 +274,12 @@ module Teckel
263
274
  other_class
264
275
  end
265
276
 
266
- def build_constructor(on, sym_or_proc)
267
- if sym_or_proc.is_a?(Symbol) && on.respond_to?(sym_or_proc)
268
- on.public_method(sym_or_proc)
269
- elsif sym_or_proc.respond_to?(:call)
277
+ private def build_constructor(on, sym_or_proc)
278
+ case sym_or_proc
279
+ when Proc
270
280
  sym_or_proc
281
+ when Symbol
282
+ on.public_method(sym_or_proc) if on.respond_to?(sym_or_proc)
271
283
  end
272
284
  end
273
285
  end
@@ -1,12 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'forwardable'
4
-
5
3
  module Teckel
6
4
  module Chain
7
5
  class Result < Teckel::Operation::Result
8
- extend Forwardable
9
-
10
6
  # @param value [Object] The result value
11
7
  # @param success [Boolean] whether this is a successful result
12
8
  # @param step [Teckel::Chain::Step]
@@ -16,13 +12,13 @@ module Teckel
16
12
  end
17
13
 
18
14
  class << self
19
- alias :[] :new
15
+ alias_method :[], :new
20
16
  end
21
17
 
22
- # @!method step
23
- # Delegates to +step.name+
24
- # @return [String,Symbol] The step name of the failed operation.
25
- def_delegator :@step, :name, :step
18
+ # @return [String,Symbol] The step name of the failed operation.
19
+ def step
20
+ @step.name
21
+ end
26
22
 
27
23
  def deconstruct
28
24
  [successful?, @step.name, value]
@@ -7,9 +7,13 @@ module Teckel
7
7
  # @!visibility protected
8
8
  class Runner
9
9
  # @!visibility private
10
- UNDEFINED = Object.new
10
+ # @return [Object]
11
+ UNDEFINED = Teckel::UNDEFINED
11
12
 
12
13
  # @!visibility private
14
+ # @attr [Object] value the return value / result of the step execution
15
+ # @attr [Boolean] success whether the step has been executed successfully
16
+ # @attr [Teckel::Chain::Step] the step instance
13
17
  StepResult = Struct.new(:value, :success, :step)
14
18
 
15
19
  def initialize(chain, settings = UNDEFINED)
@@ -29,12 +33,10 @@ module Teckel
29
33
  end
30
34
 
31
35
  def steps
32
- settings == UNDEFINED ? chain.steps : steps_with_settings
36
+ settings.eql?(UNDEFINED) ? chain.steps : steps_with_settings
33
37
  end
34
38
 
35
- private
36
-
37
- def run(input)
39
+ private def run(input)
38
40
  steps.each_with_object(StepResult.new(input)) do |step, step_result|
39
41
  result = step.operation.call(step_result.value)
40
42
 
@@ -46,11 +48,11 @@ module Teckel
46
48
  end
47
49
  end
48
50
 
49
- def step_with_settings(step)
51
+ private def step_with_settings(step)
50
52
  settings.key?(step.name) ? step.with(settings[step.name]) : step
51
53
  end
52
54
 
53
- def steps_with_settings
55
+ private def steps_with_settings
54
56
  Enumerator.new do |yielder|
55
57
  chain.steps.each do |step|
56
58
  yielder << step_with_settings(step)
data/lib/teckel/chain.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'chain/config'
4
- require_relative 'chain/step'
5
- require_relative 'chain/result'
6
- require_relative 'chain/runner'
3
+ require_relative "chain/config"
4
+ require_relative "chain/step"
5
+ require_relative "chain/result"
6
+ require_relative "chain/runner"
7
7
 
8
8
  module Teckel
9
9
  # Railway style execution of multiple Operations.
@@ -45,13 +45,13 @@ module Teckel
45
45
  # @return [Teckel::Chain::Result] The result object wrapping
46
46
  # the result value, the success state and last executed step.
47
47
  def call(input = nil)
48
- default_settings = self.default_settings
48
+ default_settings = default_settings()
49
49
 
50
50
  runner =
51
51
  if default_settings
52
- self.runner.new(self, default_settings)
52
+ runner().new(self, default_settings)
53
53
  else
54
- self.runner.new(self)
54
+ runner().new(self)
55
55
  end
56
56
 
57
57
  if around
@@ -61,21 +61,27 @@ module Teckel
61
61
  end
62
62
  end
63
63
 
64
- # @param settings [Hash{String,Symbol => Object}] Set settings for a step by it's name
64
+ # Provide settings to the configured steps.
65
+ #
66
+ # @param settings [Hash{(String,Symbol) => Object}] Set settings for a step by it's name
67
+ # @return [#call] A callable, either a {Teckel::Chain::Runner} or,
68
+ # when configured with an around hook, a +Proc+
65
69
  def with(settings)
66
- runner = self.runner.new(self, settings)
70
+ runner = runner().new(self, settings)
67
71
  if around
68
- ->(input) { around.call(runner, input) }
72
+ around.curry[runner]
69
73
  else
70
74
  runner
71
75
  end
72
76
  end
73
- alias :set :with
77
+ alias_method :set, :with
74
78
  end
75
79
 
76
80
  def self.included(receiver)
77
- receiver.extend Config
78
- receiver.extend ClassMethods
81
+ receiver.class_eval do
82
+ extend Config
83
+ extend ClassMethods
84
+ end
79
85
  end
80
86
  end
81
87
  end
data/lib/teckel/config.rb CHANGED
@@ -15,11 +15,16 @@ module Teckel
15
15
  # - sets (and returns) the blocks return value otherwise
16
16
  # - calling without +value+ and +block+ works like {Hash#[]}
17
17
  #
18
+ # @param key [Symbol,String] Name of the configuration
19
+ # @param value [Object] Value of the configuration
20
+ # @yield [key] If using in fetch mode and no value has been set
21
+ # @yieldparam key [Symbol,String] Name of the configuration
22
+ # @return [Object]
18
23
  # @raise [FrozenConfigError] When overwriting a key
19
24
  # @!visibility private
20
- def for(key, value = nil, &block)
25
+ def get_or_set(key, value = nil, &block)
21
26
  if value.nil?
22
- get_or_set(key, &block)
27
+ _get_or_set(key, &block)
23
28
  elsif @config.key?(key)
24
29
  raise FrozenConfigError, "Configuration #{key} is already set"
25
30
  else
@@ -28,6 +33,9 @@ module Teckel
28
33
  end
29
34
 
30
35
  # @!visibility private
36
+ # @param key [Symbol,String] Name of the configuration
37
+ # @yieldreturn [Object] The new setting
38
+ # @return [Object,nil] The new setting or +nil+ if not replaced
31
39
  def replace(key)
32
40
  @config[key] = yield if @config.key?(key)
33
41
  end
@@ -35,19 +43,17 @@ module Teckel
35
43
  # @!visibility private
36
44
  def freeze
37
45
  @config.freeze
38
- super
46
+ super() # standard:disable Style/SuperArguments
39
47
  end
40
48
 
41
49
  # @!visibility private
42
50
  def dup
43
- super.tap do |copy|
44
- copy.instance_variable_set(:@config, @config.dup)
45
- end
51
+ copy = super() # standard:disable Style/SuperArguments
52
+ copy.instance_variable_set(:@config, @config.dup)
53
+ copy
46
54
  end
47
55
 
48
- private
49
-
50
- def get_or_set(key, &block)
56
+ private def _get_or_set(key, &block)
51
57
  if block
52
58
  @config[key] ||= @config.fetch(key, &block)
53
59
  else
@@ -5,14 +5,14 @@ module Teckel
5
5
  # Simple contract for enforcing data to be not set or +nil+
6
6
  module None
7
7
  class << self
8
- # Always return nil
9
- # @return nil
8
+ # Always return +nil+
9
+ # @return [NilClass]
10
10
  # @raise [ArgumentError] when called with any non-nil arguments
11
11
  def [](*args)
12
12
  raise ArgumentError, "None called with arguments" if args.any?(&:itself)
13
13
  end
14
14
 
15
- alias :new :[]
15
+ alias_method :new, :[]
16
16
  end
17
17
  end
18
18
  end