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