teckel 0.4.0 → 0.5.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 +4 -4
- data/CHANGELOG.md +36 -0
- data/lib/teckel/chain.rb +11 -189
- data/lib/teckel/chain/config.rb +246 -0
- data/lib/teckel/chain/result.rb +3 -3
- data/lib/teckel/chain/runner.rb +28 -17
- data/lib/teckel/config.rb +11 -5
- data/lib/teckel/operation.rb +53 -383
- data/lib/teckel/operation/config.rb +394 -0
- data/lib/teckel/operation/runner.rb +2 -2
- data/lib/teckel/result.rb +4 -4
- data/lib/teckel/version.rb +1 -1
- data/spec/chain/default_settings_spec.rb +39 -0
- data/spec/chain/none_input_spec.rb +36 -0
- data/spec/operation/default_settings_spec.rb +94 -0
- metadata +11 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bf2c470202a0d90fbc79ed7d6c7e065634f34f6237d01d8d9772489f7ba43e94
|
4
|
+
data.tar.gz: 0b2937e820b2f61bf37d835521c6b981e95e50c3f1ef4d5e968ee38dfea7c5fa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3cf2096a7827c2547e6c4cc135657d68dcc597d057e202d0c8cda91753eee09648133e3c6728a3ae1ec14f140fe562b2648ae77439cccfa015a5130326557fba
|
7
|
+
data.tar.gz: 5f13605ac5dc726ce964a85225d1e860a22d00b8eabae78616fa998e77c94ec7251f1367f07175868e8724477bc95c27945d93e23bee0da68303a452a1180e09
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,41 @@
|
|
1
1
|
# Changes
|
2
2
|
|
3
|
+
## 0.5.0
|
4
|
+
|
5
|
+
- Fix: calling chain with settings and no input [GH-14]
|
6
|
+
- Add: Default settings for Operation and Chains [GH-17], [GH-18]
|
7
|
+
```ruby
|
8
|
+
class MyOperation
|
9
|
+
include Teckel::Operation
|
10
|
+
|
11
|
+
settings Struct.new(:logger)
|
12
|
+
|
13
|
+
# If your settings class can cope with no input and you want to make sure
|
14
|
+
# `settings` gets initialized and set.
|
15
|
+
# settings will be #<struct logger=nil>
|
16
|
+
default_settings!
|
17
|
+
|
18
|
+
# settings will be #<struct logger=MyGlobalLogger>
|
19
|
+
default_settings!(MyGlobalLogger)
|
20
|
+
|
21
|
+
# settings will be #<struct logger=#<Logger:<...>>
|
22
|
+
default_settings! -> { settings.new(Logger.new("/tmp/my.log")) }
|
23
|
+
end
|
24
|
+
|
25
|
+
class Chain
|
26
|
+
include Teckel::Chain
|
27
|
+
|
28
|
+
# set or overwrite operation settings
|
29
|
+
default_settings!(a: MyOtherLogger)
|
30
|
+
|
31
|
+
step :a, MyOperation
|
32
|
+
end
|
33
|
+
```
|
34
|
+
|
35
|
+
Internal:
|
36
|
+
- Move operation and chain config dsl methods into own module [GH-15]
|
37
|
+
- Code simplifications [GH-16]
|
38
|
+
|
3
39
|
## 0.4.0
|
4
40
|
|
5
41
|
- Moving verbose examples from API docs into github pages
|
data/lib/teckel/chain.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'chain/config'
|
3
4
|
require_relative 'chain/step'
|
4
5
|
require_relative 'chain/result'
|
5
6
|
require_relative 'chain/runner'
|
@@ -37,122 +38,6 @@ module Teckel
|
|
37
38
|
end
|
38
39
|
end
|
39
40
|
|
40
|
-
# Declare a {Operation} as a named step
|
41
|
-
#
|
42
|
-
# @param name [String,Symbol] The name of the operation.
|
43
|
-
# This name is used in an error case to let you know which step failed.
|
44
|
-
# @param operation [Operation] The operation to call, which
|
45
|
-
# must return a {Teckel::Result} object.
|
46
|
-
def step(name, operation)
|
47
|
-
steps << Step.new(name, operation)
|
48
|
-
end
|
49
|
-
|
50
|
-
# Get the list of defined steps
|
51
|
-
#
|
52
|
-
# @return [<Step>]
|
53
|
-
def steps
|
54
|
-
@config.for(:steps) { [] }
|
55
|
-
end
|
56
|
-
|
57
|
-
# Set or get the optional around hook.
|
58
|
-
# A Hook might be given as a block or anything callable. The execution of
|
59
|
-
# the chain is yielded to this hook. The first argument being the callable
|
60
|
-
# chain ({Runner}) and the second argument the +input+ data. The hook also
|
61
|
-
# needs to return the result.
|
62
|
-
#
|
63
|
-
# @param callable [Proc,{#call}] The hook to pass chain execution control to. (nil)
|
64
|
-
#
|
65
|
-
# @return [Proc,{#call}] The configured hook
|
66
|
-
#
|
67
|
-
# @example Around hook with block
|
68
|
-
# OUTPUTS = []
|
69
|
-
#
|
70
|
-
# class Echo
|
71
|
-
# include ::Teckel::Operation
|
72
|
-
# result!
|
73
|
-
#
|
74
|
-
# input Hash
|
75
|
-
# output input
|
76
|
-
#
|
77
|
-
# def call(hsh)
|
78
|
-
# hsh
|
79
|
-
# end
|
80
|
-
# end
|
81
|
-
#
|
82
|
-
# class MyChain
|
83
|
-
# include Teckel::Chain
|
84
|
-
#
|
85
|
-
# around do |chain, input|
|
86
|
-
# OUTPUTS << "before start"
|
87
|
-
# result = chain.call(input)
|
88
|
-
# OUTPUTS << "after start"
|
89
|
-
# result
|
90
|
-
# end
|
91
|
-
#
|
92
|
-
# step :noop, Echo
|
93
|
-
# end
|
94
|
-
#
|
95
|
-
# result = MyChain.call(some: 'test')
|
96
|
-
# OUTPUTS #=> ["before start", "after start"]
|
97
|
-
# result.success #=> { some: "test" }
|
98
|
-
def around(callable = nil, &block)
|
99
|
-
@config.for(:around, callable || block)
|
100
|
-
end
|
101
|
-
|
102
|
-
# @!attribute [r] runner()
|
103
|
-
# @return [Class] The Runner class
|
104
|
-
# @!visibility protected
|
105
|
-
|
106
|
-
# Overwrite the default runner
|
107
|
-
# @param klass [Class] A class like the {Runner}
|
108
|
-
# @!visibility protected
|
109
|
-
def runner(klass = nil)
|
110
|
-
@config.for(:runner, klass) { Runner }
|
111
|
-
end
|
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
|
-
|
156
41
|
# The primary interface to call the chain with the given input.
|
157
42
|
#
|
158
43
|
# @param input Any form of input the first steps +input+ class can handle
|
@@ -160,7 +45,15 @@ module Teckel
|
|
160
45
|
# @return [Teckel::Chain::Result] The result object wrapping
|
161
46
|
# the result value, the success state and last executed step.
|
162
47
|
def call(input = nil)
|
163
|
-
|
48
|
+
default_settings = self.default_settings
|
49
|
+
|
50
|
+
runner =
|
51
|
+
if default_settings
|
52
|
+
self.runner.new(self, default_settings)
|
53
|
+
else
|
54
|
+
self.runner.new(self)
|
55
|
+
end
|
56
|
+
|
164
57
|
if around
|
165
58
|
around.call(runner, input)
|
166
59
|
else
|
@@ -178,81 +71,10 @@ module Teckel
|
|
178
71
|
end
|
179
72
|
end
|
180
73
|
alias :set :with
|
181
|
-
|
182
|
-
# @!visibility private
|
183
|
-
# @return [void]
|
184
|
-
def define!
|
185
|
-
raise MissingConfigError, "Cannot define Chain with no steps" if steps.empty?
|
186
|
-
|
187
|
-
%i[around runner result result_constructor].each { |e| public_send(e) }
|
188
|
-
steps.each(&:finalize!)
|
189
|
-
nil
|
190
|
-
end
|
191
|
-
|
192
|
-
# Disallow any further changes to this Chain.
|
193
|
-
# @note This also calls +finalize!+ on all Operations defined as steps.
|
194
|
-
#
|
195
|
-
# @return [self] Frozen self
|
196
|
-
# @!visibility public
|
197
|
-
def finalize!
|
198
|
-
define!
|
199
|
-
steps.freeze
|
200
|
-
@config.freeze
|
201
|
-
self
|
202
|
-
end
|
203
|
-
|
204
|
-
# Produces a shallow copy of this chain.
|
205
|
-
# It's {around}, {runner} and {steps} will get +dup+'ed
|
206
|
-
#
|
207
|
-
# @return [self]
|
208
|
-
# @!visibility public
|
209
|
-
def dup
|
210
|
-
dup_config(super)
|
211
|
-
end
|
212
|
-
|
213
|
-
# Produces a clone of this chain.
|
214
|
-
# It's {around}, {runner} and {steps} will get +dup+'ed
|
215
|
-
#
|
216
|
-
# @return [self]
|
217
|
-
# @!visibility public
|
218
|
-
def clone
|
219
|
-
if frozen?
|
220
|
-
super
|
221
|
-
else
|
222
|
-
dup_config(super)
|
223
|
-
end
|
224
|
-
end
|
225
|
-
|
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
|
251
|
-
end
|
252
|
-
end
|
253
74
|
end
|
254
75
|
|
255
76
|
def self.included(receiver)
|
77
|
+
receiver.extend Config
|
256
78
|
receiver.extend ClassMethods
|
257
79
|
end
|
258
80
|
end
|
@@ -0,0 +1,246 @@
|
|
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
|
+
# 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
|
+
#
|
107
|
+
# class Result < Teckel::Operation::Result
|
108
|
+
# def initialize(value, success, step, options = {}); end
|
109
|
+
# end
|
110
|
+
#
|
111
|
+
# # If you need more control over how to build a new +Settings+ instance
|
112
|
+
# result_constructor ->(value, success, step) { result.new(value, success, step, {foo: :bar}) }
|
113
|
+
# end
|
114
|
+
def result_constructor(sym_or_proc = nil)
|
115
|
+
constructor = build_constructor(result, sym_or_proc) unless sym_or_proc.nil?
|
116
|
+
|
117
|
+
@config.for(:result_constructor, constructor) {
|
118
|
+
build_constructor(result, Teckel::DEFAULT_CONSTRUCTOR)
|
119
|
+
} || raise(MissingConfigError, "Missing result_constructor config for #{self}")
|
120
|
+
end
|
121
|
+
|
122
|
+
# Declare default settings operation iin this chain should use when called without
|
123
|
+
# {Teckel::Chain::ClassMethods#with #with}.
|
124
|
+
#
|
125
|
+
# Explicit call-time settings will *not* get merged with declared default setting.
|
126
|
+
#
|
127
|
+
# @param settings [Hash{String,Symbol => Object}] Set settings for a step by it's name
|
128
|
+
#
|
129
|
+
# @example
|
130
|
+
# class MyOperation
|
131
|
+
# include Teckel::Operation
|
132
|
+
# result!
|
133
|
+
#
|
134
|
+
# settings Struct.new(:say, :other)
|
135
|
+
# settings_constructor ->(data) { settings.new(*data.values_at(*settings.members)) }
|
136
|
+
#
|
137
|
+
# input none
|
138
|
+
# output Hash
|
139
|
+
# error none
|
140
|
+
#
|
141
|
+
# def call(_)
|
142
|
+
# settings.to_h
|
143
|
+
# end
|
144
|
+
# end
|
145
|
+
#
|
146
|
+
# class Chain
|
147
|
+
# include Teckel::Chain
|
148
|
+
#
|
149
|
+
# default_settings!(a: { say: "Chain Default" })
|
150
|
+
#
|
151
|
+
# step :a, MyOperation
|
152
|
+
# end
|
153
|
+
#
|
154
|
+
# # Using the chains default settings
|
155
|
+
# result = Chain.call
|
156
|
+
# result.success #=> {say: "Chain Default", other: nil}
|
157
|
+
#
|
158
|
+
# # explicit settings passed via `with` will overwrite all defaults
|
159
|
+
# result = Chain.with(a: { other: "What" }).call
|
160
|
+
# result.success #=> {say: nil, other: "What"}
|
161
|
+
def default_settings!(settings) # :nodoc: The bang is for consistency with the Operation class
|
162
|
+
@config.for(:default_settings, settings)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Getter for configured default settings
|
166
|
+
# @return [nil|#call] The callable constructor
|
167
|
+
def default_settings
|
168
|
+
@config.for(:default_settings)
|
169
|
+
end
|
170
|
+
|
171
|
+
REQUIRED_CONFIGS = %i[around runner result result_constructor].freeze
|
172
|
+
|
173
|
+
# @!visibility private
|
174
|
+
# @return [void]
|
175
|
+
def define!
|
176
|
+
raise MissingConfigError, "Cannot define Chain with no steps" if steps.empty?
|
177
|
+
|
178
|
+
REQUIRED_CONFIGS.each { |e| public_send(e) }
|
179
|
+
steps.each(&:finalize!)
|
180
|
+
nil
|
181
|
+
end
|
182
|
+
|
183
|
+
# Disallow any further changes to this Chain.
|
184
|
+
# @note This also calls +finalize!+ on all Operations defined as steps.
|
185
|
+
#
|
186
|
+
# @return [self] Frozen self
|
187
|
+
# @!visibility public
|
188
|
+
def finalize!
|
189
|
+
define!
|
190
|
+
steps.freeze
|
191
|
+
@config.freeze
|
192
|
+
self
|
193
|
+
end
|
194
|
+
|
195
|
+
# Produces a shallow copy of this chain.
|
196
|
+
# It's {around}, {runner} and {steps} will get +dup+'ed
|
197
|
+
#
|
198
|
+
# @return [self]
|
199
|
+
# @!visibility public
|
200
|
+
def dup
|
201
|
+
dup_config(super)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Produces a clone of this chain.
|
205
|
+
# It's {around}, {runner} and {steps} will get +dup+'ed
|
206
|
+
#
|
207
|
+
# @return [self]
|
208
|
+
# @!visibility public
|
209
|
+
def clone
|
210
|
+
if frozen?
|
211
|
+
super
|
212
|
+
else
|
213
|
+
dup_config(super)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# @!visibility private
|
218
|
+
def inherited(subclass)
|
219
|
+
dup_config(subclass)
|
220
|
+
end
|
221
|
+
|
222
|
+
# @!visibility private
|
223
|
+
def self.extended(base)
|
224
|
+
base.instance_variable_set(:@config, Teckel::Config.new)
|
225
|
+
end
|
226
|
+
|
227
|
+
private
|
228
|
+
|
229
|
+
def dup_config(other_class)
|
230
|
+
new_config = @config.dup
|
231
|
+
new_config.replace(:steps) { steps.dup }
|
232
|
+
|
233
|
+
other_class.instance_variable_set(:@config, new_config)
|
234
|
+
other_class
|
235
|
+
end
|
236
|
+
|
237
|
+
def build_constructor(on, sym_or_proc)
|
238
|
+
if sym_or_proc.is_a?(Symbol) && on.respond_to?(sym_or_proc)
|
239
|
+
on.public_method(sym_or_proc)
|
240
|
+
elsif sym_or_proc.respond_to?(:call)
|
241
|
+
sym_or_proc
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|