teckel 0.4.0 → 0.8.0

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