teckel 0.4.0 → 0.8.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: 18179b7fd315a11bc51f0ac4cfbdba4c0e74ad82a6a7dc186289433906ec2783
4
- data.tar.gz: 02eb5f5c59ac9983a92b457f9253a38f9188e3f2b17d4f30029aa545b3151181
3
+ metadata.gz: 213ed6f9b3d25db8b7fc284ced55aa1a3a71b4ec54f10fd3ed3bf4e0a315dbd3
4
+ data.tar.gz: d013f110763117b066987bfb6c085e8682db372856ea7ab27f4803858be96e3c
5
5
  SHA512:
6
- metadata.gz: cbbbebfecccf6806da642dbc1dce843946c7dcaa27abb7c38467b0512ff5cb32d13b848fae0eaa0f8c4641f26e5b1f03f58f82fb70b3aa8aa1a891356698ff16
7
- data.tar.gz: b312534680f31d7cff2a13326f3dc3cc01566110d3001b285d233c97548edbfb2fe008d59821ef0723b63687f12e95cca3f06f2f7c1a3f8d0e15d90caca4733d
6
+ metadata.gz: 82db9106f0da688411be738866692455653eb59298afa01ba24500975b0498bf9a164f9f59e2741a4ed9c0c252174bd86fa71dd4204338fe04424d961c21621b
7
+ data.tar.gz: 6c693373c212e3b127dde042fdaba1ab7865d813cba17d0624a73a62e62589fc68bfe86ed93b9e7c32295deb6053411e4ea0cf57f0897b6f8e7e339ce60dd7be
data/CHANGELOG.md CHANGED
@@ -1,5 +1,76 @@
1
1
  # Changes
2
2
 
3
+ ## 0.8.0
4
+
5
+ - Add mutation testing (currently about 80% covered)
6
+ - Breaking: `Teckel::Operation::Result` no longer converts the `successful` value to a boolean.
7
+ When using the default result implementation, nothing changes for you.
8
+ When you manually pass anything else into it, `succesful?` will return this value. The `failure` and `success` methods
9
+ work on the "Truthy" value of `successful`
10
+ ```ruby
11
+ result = Result.new(some_value, 42)
12
+ result.successful?
13
+ # => 42
14
+ ```
15
+ - Change: `freeze`ing an `Operation` or `Chain` will also freeze their internal config (input, output, steps, etc.)
16
+
17
+ Internal:
18
+
19
+ - Some refactoring to cover mutations
20
+ - Extracted creating the runable `Operation` instance into a new, public `runable(settings = UNDEFINED)` method.
21
+ Mainly to reduce code duplication but this also makes testing, stubbing and mocking easier.
22
+
23
+ ## 0.7.0
24
+
25
+ - Breaking: `Teckel::Chain` will not be required by default. require manually if needed `require "teckel/chain"` [GH-24]
26
+ - Breaking: Internally, `Teckel::Operation::Runner` instead of `:success` and `:failure` now only uses `:halt` as it's throw-catch symbol. [GH-26]
27
+ - Add: Using the default `Teckel::Operation::Runner`, `input_constructor` and `result_constructor` will be executed
28
+ within the context of the operation instance. This allows for `input_constructor` to call `fail!` and `success!`
29
+ without ever `call`ing the operation. [GH-26]
30
+
31
+
32
+ ## 0.6.0
33
+
34
+ - Breaking: Operations return values will be ignored. [GH-21]
35
+ * You'll need to use `success!` or `failure!`
36
+ * `success!` and `failure!` are now implemented on the `Runner`, which makes it easier to change their behavior (including the one above).
37
+
38
+ ## 0.5.0
39
+
40
+ - Fix: calling chain with settings and no input [GH-14]
41
+ - Add: Default settings for Operation and Chains [GH-17], [GH-18]
42
+ ```ruby
43
+ class MyOperation
44
+ include Teckel::Operation
45
+
46
+ settings Struct.new(:logger)
47
+
48
+ # If your settings class can cope with no input and you want to make sure
49
+ # `settings` gets initialized and set.
50
+ # settings will be #<struct logger=nil>
51
+ default_settings!
52
+
53
+ # settings will be #<struct logger=MyGlobalLogger>
54
+ default_settings!(MyGlobalLogger)
55
+
56
+ # settings will be #<struct logger=#<Logger:<...>>
57
+ default_settings! -> { settings.new(Logger.new("/tmp/my.log")) }
58
+ end
59
+
60
+ class Chain
61
+ include Teckel::Chain
62
+
63
+ # set or overwrite operation settings
64
+ default_settings!(a: MyOtherLogger)
65
+
66
+ step :a, MyOperation
67
+ end
68
+ ```
69
+
70
+ Internal:
71
+ - Move operation and chain config dsl methods into own module [GH-15]
72
+ - Code simplifications [GH-16]
73
+
3
74
  ## 0.4.0
4
75
 
5
76
  - Moving verbose examples from API docs into github pages
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
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teckel
4
+ module Chain
5
+ module Config
6
+ # Declare a {Operation} as a named step
7
+ #
8
+ # @param name [String,Symbol] The name of the operation.
9
+ # This name is used in an error case to let you know which step failed.
10
+ # @param operation [Operation] The operation to call, which
11
+ # must return a {Teckel::Result} object.
12
+ def step(name, operation)
13
+ steps << Step.new(name, operation)
14
+ end
15
+
16
+ # Get the list of defined steps
17
+ #
18
+ # @return [<Step>]
19
+ def steps
20
+ @config.for(:steps) { [] }
21
+ end
22
+
23
+ # Set or get the optional around hook.
24
+ # A Hook might be given as a block or anything callable. The execution of
25
+ # the chain is yielded to this hook. The first argument being the callable
26
+ # chain ({Runner}) and the second argument the +input+ data. The hook also
27
+ # needs to return the result.
28
+ #
29
+ # @param callable [Proc,{#call}] The hook to pass chain execution control to. (nil)
30
+ #
31
+ # @return [Proc,{#call}] The configured hook
32
+ #
33
+ # @example Around hook with block
34
+ # OUTPUTS = []
35
+ #
36
+ # class Echo
37
+ # include ::Teckel::Operation
38
+ # result!
39
+ #
40
+ # input Hash
41
+ # output input
42
+ #
43
+ # def call(hsh)
44
+ # success!(hsh)
45
+ # end
46
+ # end
47
+ #
48
+ # class MyChain
49
+ # include Teckel::Chain
50
+ #
51
+ # around do |chain, input|
52
+ # OUTPUTS << "before start"
53
+ # result = chain.call(input)
54
+ # OUTPUTS << "after start"
55
+ # result
56
+ # end
57
+ #
58
+ # step :noop, Echo
59
+ # end
60
+ #
61
+ # result = MyChain.call(some: 'test')
62
+ # OUTPUTS #=> ["before start", "after start"]
63
+ # result.success #=> { some: "test" }
64
+ def around(callable = nil, &block)
65
+ @config.for(:around, callable || block)
66
+ end
67
+
68
+ # @!attribute [r] runner()
69
+ # @return [Class] The Runner class
70
+ # @!visibility protected
71
+
72
+ # Overwrite the default runner
73
+ # @param klass [Class] A class like the {Runner}
74
+ # @!visibility protected
75
+ def runner(klass = nil)
76
+ @config.for(:runner, klass) { Runner }
77
+ end
78
+
79
+ # @overload result()
80
+ # Get the configured result object class wrapping {.error} or {.output}.
81
+ # @return [Class] The +result+ class, or {Teckel::Chain::Result} as default
82
+ #
83
+ # @overload result(klass)
84
+ # Set the result object class wrapping {.error} or {.output}.
85
+ # @param klass [Class] The +result+ class
86
+ # @return [Class] The +result+ class configured
87
+ def result(klass = nil)
88
+ @config.for(:result, klass) { const_defined?(:Result, false) ? self::Result : Teckel::Chain::Result }
89
+ end
90
+
91
+ # @overload result_constructor()
92
+ # The callable constructor to build an instance of the +result+ class.
93
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
94
+ # @return [Proc] A callable that will return an instance of +result+ class.
95
+ #
96
+ # @overload result_constructor(sym_or_proc)
97
+ # Define how to build the +result+.
98
+ # @param sym_or_proc [Symbol, #call]
99
+ # - Either a +Symbol+ representing the _public_ method to call on the +result+ class.
100
+ # - Or anything that response to +#call+ (like a +Proc+).
101
+ # @return [#call] The callable constructor
102
+ #
103
+ # @example
104
+ # class MyOperation
105
+ # include Teckel::Operation
106
+ # result!
107
+ #
108
+ # settings Struct.new(:say, :other)
109
+ # settings_constructor ->(data) { settings.new(*data.values_at(*settings.members)) }
110
+ #
111
+ # input none
112
+ # output Hash
113
+ # error none
114
+ #
115
+ # def call(_)
116
+ # success!(settings.to_h)
117
+ # end
118
+ # end
119
+ #
120
+ # class Chain
121
+ # include Teckel::Chain
122
+ #
123
+ # class Result < Teckel::Operation::Result
124
+ # def initialize(value, success, step, opts = {})
125
+ # super(value, success)
126
+ # @step = step
127
+ # @opts = opts
128
+ # end
129
+ #
130
+ # class << self
131
+ # alias :[] :new # Alias the default constructor to :new
132
+ # end
133
+ #
134
+ # attr_reader :opts, :step
135
+ # end
136
+ #
137
+ # result_constructor ->(value, success, step) {
138
+ # result.new(value, success, step, time: Time.now.to_i)
139
+ # }
140
+ #
141
+ # step :a, MyOperation
142
+ # end
143
+ def result_constructor(sym_or_proc = nil)
144
+ constructor = build_constructor(result, sym_or_proc) unless sym_or_proc.nil?
145
+
146
+ @config.for(:result_constructor, constructor) {
147
+ build_constructor(result, Teckel::DEFAULT_CONSTRUCTOR)
148
+ } || raise(MissingConfigError, "Missing result_constructor config for #{self}")
149
+ end
150
+
151
+ # Declare default settings operation in this chain should use when called without
152
+ # {Teckel::Chain::ClassMethods#with #with}.
153
+ #
154
+ # Explicit call-time settings will *not* get merged with declared default setting.
155
+ #
156
+ # @param settings [Hash{String,Symbol => Object}] Set settings for a step by it's name
157
+ #
158
+ # @example
159
+ # class MyOperation
160
+ # include Teckel::Operation
161
+ # result!
162
+ #
163
+ # settings Struct.new(:say, :other)
164
+ # settings_constructor ->(data) { settings.new(*data.values_at(*settings.members)) }
165
+ #
166
+ # input none
167
+ # output Hash
168
+ # error none
169
+ #
170
+ # def call(_)
171
+ # success!(settings.to_h)
172
+ # end
173
+ # end
174
+ #
175
+ # class Chain
176
+ # include Teckel::Chain
177
+ #
178
+ # default_settings!(a: { say: "Chain Default" })
179
+ #
180
+ # step :a, MyOperation
181
+ # end
182
+ #
183
+ # # Using the chains default settings
184
+ # result = Chain.call
185
+ # result.success #=> {say: "Chain Default", other: nil}
186
+ #
187
+ # # explicit settings passed via `with` will overwrite all defaults
188
+ # result = Chain.with(a: { other: "What" }).call
189
+ # result.success #=> {say: nil, other: "What"}
190
+ def default_settings!(settings) # :nodoc: The bang is for consistency with the Operation class
191
+ @config.for(:default_settings, settings)
192
+ end
193
+
194
+ # Getter for configured default settings
195
+ # @return [nil|#call] The callable constructor
196
+ def default_settings
197
+ @config.for(:default_settings)
198
+ end
199
+
200
+ REQUIRED_CONFIGS = %i[around runner result result_constructor].freeze
201
+
202
+ # @!visibility private
203
+ # @return [void]
204
+ def define!
205
+ raise MissingConfigError, "Cannot define Chain with no steps" if steps.empty?
206
+
207
+ REQUIRED_CONFIGS.each { |e| public_send(e) }
208
+ steps.each(&:finalize!)
209
+ nil
210
+ end
211
+
212
+ # Disallow any further changes to this Chain.
213
+ # @note This also calls +finalize!+ on all Operations defined as steps.
214
+ #
215
+ # @return [self] Frozen self
216
+ # @!visibility public
217
+ def finalize!
218
+ define!
219
+ steps.freeze
220
+ @config.freeze
221
+ self
222
+ end
223
+
224
+ # Produces a shallow copy of this chain.
225
+ # It's {around}, {runner} and {steps} will get +dup+'ed
226
+ #
227
+ # @return [self]
228
+ # @!visibility public
229
+ def dup
230
+ dup_config(super())
231
+ end
232
+
233
+ # Produces a clone of this chain.
234
+ # It's {around}, {runner} and {steps} will get +dup+'ed
235
+ #
236
+ # @return [self]
237
+ # @!visibility public
238
+ def clone
239
+ if frozen?
240
+ super()
241
+ else
242
+ dup_config(super())
243
+ end
244
+ end
245
+
246
+ # Prevents further modifications to this chain and its config
247
+ #
248
+ # @return [self]
249
+ # @!visibility public
250
+ def freeze
251
+ steps.freeze
252
+ @config.freeze
253
+ super()
254
+ end
255
+
256
+ # @!visibility private
257
+ def inherited(subclass)
258
+ super(dup_config(subclass))
259
+ end
260
+
261
+ # @!visibility private
262
+ def self.extended(base)
263
+ base.instance_variable_set(:@config, Teckel::Config.new)
264
+ end
265
+
266
+ private
267
+
268
+ def dup_config(other_class)
269
+ new_config = @config.dup
270
+ new_config.replace(:steps) { steps.dup }
271
+
272
+ other_class.instance_variable_set(:@config, new_config)
273
+ other_class
274
+ end
275
+
276
+ def build_constructor(on, sym_or_proc)
277
+ case sym_or_proc
278
+ when Proc
279
+ sym_or_proc
280
+ when Symbol
281
+ on.public_method(sym_or_proc) if on.respond_to?(sym_or_proc)
282
+ end
283
+ end
284
+ end
285
+ end
286
+ end
@@ -29,9 +29,9 @@ module Teckel
29
29
  end
30
30
 
31
31
  def deconstruct_keys(keys)
32
- super.tap { |e|
33
- e[:step] = @step.name if keys.include?(:step)
34
- }
32
+ e = super
33
+ e[:step] = @step.name if keys.include?(:step)
34
+ e
35
35
  end
36
36
  end
37
37
  end
@@ -9,6 +9,9 @@ module Teckel
9
9
  # @!visibility private
10
10
  UNDEFINED = Object.new
11
11
 
12
+ # @!visibility private
13
+ StepResult = Struct.new(:value, :success, :step)
14
+
12
15
  def initialize(chain, settings = UNDEFINED)
13
16
  @chain, @settings = chain, settings
14
17
  end
@@ -20,29 +23,37 @@ module Teckel
20
23
  #
21
24
  # @return [Teckel::Chain::Result] The result object wrapping
22
25
  # either the success or failure value.
23
- def call(input)
24
- last_result = nil
25
- last_step = nil
26
- steps.each do |step|
27
- last_step = step
28
- value = last_result ? last_result.value : input
26
+ def call(input = nil)
27
+ step_result = run(input)
28
+ chain.result_constructor.call(*step_result)
29
+ end
30
+
31
+ def steps
32
+ settings.eql?(UNDEFINED) ? chain.steps : steps_with_settings
33
+ end
34
+
35
+ private
36
+
37
+ def run(input)
38
+ steps.each_with_object(StepResult.new(input)) do |step, step_result|
39
+ result = step.operation.call(step_result.value)
29
40
 
30
- last_result = step.operation.call(value)
41
+ step_result.step = step
42
+ step_result.value = result.value
43
+ step_result.success = result.successful?
31
44
 
32
- break if last_result.failure?
45
+ break step_result if result.failure?
33
46
  end
47
+ end
34
48
 
35
- chain.result_constructor.call(last_result.value, last_result.successful?, last_step)
49
+ def step_with_settings(step)
50
+ settings.key?(step.name) ? step.with(settings[step.name]) : step
36
51
  end
37
52
 
38
- def steps
39
- if settings == UNDEFINED
40
- chain.steps
41
- else
42
- Enumerator.new do |yielder|
43
- chain.steps.each do |step|
44
- yielder << (settings.key?(step.name) ? step.with(settings[step.name]) : step)
45
- end
53
+ def steps_with_settings
54
+ Enumerator.new do |yielder|
55
+ chain.steps.each do |step|
56
+ yielder << step_with_settings(step)
46
57
  end
47
58
  end
48
59
  end