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.
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
- runner = self.runner.new(self)
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,82 +71,13 @@ 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)
256
- receiver.extend ClassMethods
77
+ receiver.class_eval do
78
+ extend Config
79
+ extend ClassMethods
80
+ end
257
81
  end
258
82
  end
259
83
  end
data/lib/teckel/config.rb CHANGED
@@ -19,11 +19,7 @@ module Teckel
19
19
  # @!visibility private
20
20
  def for(key, value = nil, &block)
21
21
  if value.nil?
22
- if block
23
- @config[key] ||= @config.fetch(key, &block)
24
- else
25
- @config[key]
26
- end
22
+ get_or_set(key, &block)
27
23
  elsif @config.key?(key)
28
24
  raise FrozenConfigError, "Configuration #{key} is already set"
29
25
  else
@@ -39,14 +35,24 @@ module Teckel
39
35
  # @!visibility private
40
36
  def freeze
41
37
  @config.freeze
42
- super
38
+ super()
43
39
  end
44
40
 
45
41
  # @!visibility private
46
42
  def dup
47
- super.tap do |copy|
43
+ super().tap do |copy|
48
44
  copy.instance_variable_set(:@config, @config.dup)
49
45
  end
50
46
  end
47
+
48
+ private
49
+
50
+ def get_or_set(key, &block)
51
+ if block
52
+ @config[key] ||= @config.fetch(key, &block)
53
+ else
54
+ @config[key]
55
+ end
56
+ end
51
57
  end
52
58
  end
@@ -0,0 +1,400 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teckel
4
+ module Operation
5
+ module Config
6
+ # @overload input()
7
+ # Get the configured class wrapping the input data structure.
8
+ # @return [Class] The +input+ class
9
+ # @overload input(klass)
10
+ # Set the class wrapping the input data structure.
11
+ # @param klass [Class] The +input+ class
12
+ # @return [Class] The +input+ class
13
+ def input(klass = nil)
14
+ @config.for(:input, klass) { self::Input if const_defined?(:Input) } ||
15
+ raise(MissingConfigError, "Missing input config for #{self}")
16
+ end
17
+
18
+ # @overload input_constructor()
19
+ # The callable constructor to build an instance of the +input+ class.
20
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
21
+ # @return [Proc] A callable that will return an instance of the +input+ class.
22
+ #
23
+ # @overload input_constructor(sym_or_proc)
24
+ # Define how to build the +input+.
25
+ # @param sym_or_proc [Symbol, #call]
26
+ # - Either a +Symbol+ representing the _public_ method to call on the +input+ class.
27
+ # - Or anything that response to +#call+ (like a +Proc+).
28
+ # @return [#call] The callable constructor
29
+ #
30
+ # @example simple symbol to method constructor
31
+ # class MyOperation
32
+ # include Teckel::Operation
33
+ #
34
+ # class Input
35
+ # def initialize(name:, age:); end
36
+ # end
37
+ #
38
+ # # If you need more control over how to build a new +Input+ instance
39
+ # # MyOperation.call(name: "Bob", age: 23) # -> Input.new(name: "Bob", age: 23)
40
+ # input_constructor :new
41
+ # end
42
+ #
43
+ # MyOperation.input_constructor.is_a?(Method) #=> true
44
+ #
45
+ # @example Custom Proc constructor
46
+ # class MyOperation
47
+ # include Teckel::Operation
48
+ #
49
+ # class Input
50
+ # def initialize(*args, **opts); end
51
+ # end
52
+ #
53
+ # # If you need more control over how to build a new +Input+ instance
54
+ # # MyOperation.call("foo", opt: "bar") # -> Input.new(name: "foo", opt: "bar")
55
+ # input_constructor ->(name, options) { Input.new(name: name, **options) }
56
+ # end
57
+ #
58
+ # MyOperation.input_constructor.is_a?(Proc) #=> true
59
+ def input_constructor(sym_or_proc = nil)
60
+ get_set_constructor(:input_constructor, input, sym_or_proc)
61
+ end
62
+
63
+ # @overload output()
64
+ # Get the configured class wrapping the output data structure.
65
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
66
+ # @return [Class] The +output+ class
67
+ #
68
+ # @overload output(klass)
69
+ # Set the class wrapping the output data structure.
70
+ # @param klass [Class] The +output+ class
71
+ # @return [Class] The +output+ class
72
+ def output(klass = nil)
73
+ @config.for(:output, klass) { self::Output if const_defined?(:Output) } ||
74
+ raise(MissingConfigError, "Missing output config for #{self}")
75
+ end
76
+
77
+ # @overload output_constructor()
78
+ # The callable constructor to build an instance of the +output+ class.
79
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
80
+ # @return [Proc] A callable that will return an instance of +output+ class.
81
+ #
82
+ # @overload output_constructor(sym_or_proc)
83
+ # Define how to build the +output+.
84
+ # @param sym_or_proc [Symbol, #call]
85
+ # - Either a +Symbol+ representing the _public_ method to call on the +output+ class.
86
+ # - Or anything that response to +#call+ (like a +Proc+).
87
+ # @return [#call] The callable constructor
88
+ #
89
+ # @example
90
+ # class MyOperation
91
+ # include Teckel::Operation
92
+ #
93
+ # class Output
94
+ # def initialize(*args, **opts); end
95
+ # end
96
+ #
97
+ # # MyOperation.call("foo", "bar") # -> Output.new("foo", "bar")
98
+ # output_constructor :new
99
+ #
100
+ # # If you need more control over how to build a new +Output+ instance
101
+ # # MyOperation.call("foo", opt: "bar") # -> Output.new(name: "foo", opt: "bar")
102
+ # output_constructor ->(name, options) { Output.new(name: name, **options) }
103
+ # end
104
+ def output_constructor(sym_or_proc = nil)
105
+ get_set_constructor(:output_constructor, output, sym_or_proc)
106
+ end
107
+
108
+ # @overload error()
109
+ # Get the configured class wrapping the error data structure.
110
+ # @return [Class] The +error+ class
111
+ #
112
+ # @overload error(klass)
113
+ # Set the class wrapping the error data structure.
114
+ # @param klass [Class] The +error+ class
115
+ # @return [Class,nil] The +error+ class or +nil+ if it does not error
116
+ def error(klass = nil)
117
+ @config.for(:error, klass) { self::Error if const_defined?(:Error) } ||
118
+ raise(MissingConfigError, "Missing error config for #{self}")
119
+ end
120
+
121
+ # @overload error_constructor()
122
+ # The callable constructor to build an instance of the +error+ class.
123
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
124
+ # @return [Proc] A callable that will return an instance of +error+ class.
125
+ #
126
+ # @overload error_constructor(sym_or_proc)
127
+ # Define how to build the +error+.
128
+ # @param sym_or_proc [Symbol, #call]
129
+ # - Either a +Symbol+ representing the _public_ method to call on the +error+ class.
130
+ # - Or anything that response to +#call+ (like a +Proc+).
131
+ # @return [#call] The callable constructor
132
+ #
133
+ # @example
134
+ # class MyOperation
135
+ # include Teckel::Operation
136
+ #
137
+ # class Error
138
+ # def initialize(*args, **opts); end
139
+ # end
140
+ #
141
+ # # MyOperation.call("foo", "bar") # -> Error.new("foo", "bar")
142
+ # error_constructor :new
143
+ #
144
+ # # If you need more control over how to build a new +Error+ instance
145
+ # # MyOperation.call("foo", opt: "bar") # -> Error.new(name: "foo", opt: "bar")
146
+ # error_constructor ->(name, options) { Error.new(name: name, **options) }
147
+ # end
148
+ def error_constructor(sym_or_proc = nil)
149
+ get_set_constructor(:error_constructor, error, sym_or_proc)
150
+ end
151
+
152
+ # @!endgroup
153
+
154
+ # @overload settings()
155
+ # Get the configured class wrapping the settings data structure.
156
+ # @return [Class] The +settings+ class, or {Teckel::Contracts::None} as default
157
+ #
158
+ # @overload settings(klass)
159
+ # Set the class wrapping the settings data structure.
160
+ # @param klass [Class] The +settings+ class
161
+ # @return [Class] The +settings+ class configured
162
+ def settings(klass = nil)
163
+ @config.for(:settings, klass) { const_defined?(:Settings) ? self::Settings : none }
164
+ end
165
+
166
+ # @overload settings_constructor()
167
+ # The callable constructor to build an instance of the +settings+ class.
168
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
169
+ # @return [Proc] A callable that will return an instance of +settings+ class.
170
+ #
171
+ # @overload settings_constructor(sym_or_proc)
172
+ # Define how to build the +settings+.
173
+ # @param sym_or_proc [Symbol, #call]
174
+ # - Either a +Symbol+ representing the _public_ method to call on the +settings+ class.
175
+ # - Or anything that response to +#call+ (like a +Proc+).
176
+ # @return [#call] The callable constructor
177
+ #
178
+ # @example
179
+ # class MyOperation
180
+ # include Teckel::Operation
181
+ #
182
+ # class Settings
183
+ # def initialize(*args); end
184
+ # end
185
+ #
186
+ # # MyOperation.with("foo", "bar") # -> Settings.new("foo", "bar")
187
+ # settings_constructor :new
188
+ # end
189
+ def settings_constructor(sym_or_proc = nil)
190
+ get_set_constructor(:settings_constructor, settings, sym_or_proc) ||
191
+ raise(MissingConfigError, "Missing settings_constructor config for #{self}")
192
+ end
193
+
194
+ # Declare default settings this operation should use when called without
195
+ # {Teckel::Operation::ClassMethods#with #with}.
196
+ # When executing a Operation, +settings+ will no longer be +nil+, but
197
+ # whatever you define here.
198
+ #
199
+ # Explicit call-time settings will *not* get merged with declared default setting.
200
+ #
201
+ # @overload default_settings!()
202
+ # When this operation is called without {Teckel::Operation::ClassMethods#with #with},
203
+ # +settings+ will be an instance of the +settings+ class, initialized with no arguments.
204
+ #
205
+ # @overload default_settings!(sym_or_proc)
206
+ # When this operation is called without {Teckel::Operation::ClassMethods#with #with},
207
+ # +settings+ will be an instance of this callable constructor.
208
+ #
209
+ # @param sym_or_proc [Symbol, #call]
210
+ # - Either a +Symbol+ representing the _public_ method to call on the +settings+ class.
211
+ # - Or anything that responds to +#call+ (like a +Proc+).
212
+ #
213
+ # @overload default_settings!(arg1, arg2, ...)
214
+ # When this operation is called without {Teckel::Operation::ClassMethods#with #with},
215
+ # +settings+ will be an instance of the +settings+ class, initialized with those arguments.
216
+ #
217
+ # (Like calling +MyOperation.with(arg1, arg2, ...)+)
218
+ def default_settings!(*args)
219
+ callable = if args.size.equal?(1)
220
+ build_constructor(settings, args.first)
221
+ end
222
+
223
+ callable ||= -> { settings_constructor.call(*args) }
224
+
225
+ @config.for(:default_settings, callable)
226
+ end
227
+
228
+ # Getter for configured default settings
229
+ # @return [nil|#call] The callable constructor
230
+ def default_settings
231
+ @config.for(:default_settings)
232
+ end
233
+
234
+ # @overload runner()
235
+ # @return [Class] The Runner class
236
+ # @!visibility protected
237
+ #
238
+ # @overload runner(klass)
239
+ # Overwrite the default runner
240
+ # @param klass [Class] A class like the {Runner}
241
+ # @!visibility protected
242
+ def runner(klass = nil)
243
+ @config.for(:runner, klass) { Runner }
244
+ end
245
+
246
+ # @overload result()
247
+ # Get the configured result object class wrapping {error} or {output}.
248
+ # The {ValueResult} default will act as a pass-through and does. Any error
249
+ # or output will just returned as-is.
250
+ # @return [Class] The +result+ class, or {ValueResult} as default
251
+ #
252
+ # @overload result(klass)
253
+ # Set the result object class wrapping {error} or {output}.
254
+ # @param klass [Class] The +result+ class
255
+ # @return [Class] The +result+ class configured
256
+ def result(klass = nil)
257
+ @config.for(:result, klass) { const_defined?(:Result, false) ? self::Result : ValueResult }
258
+ end
259
+
260
+ # @overload result_constructor()
261
+ # The callable constructor to build an instance of the +result+ class.
262
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
263
+ # @return [Proc] A callable that will return an instance of +result+ class.
264
+ #
265
+ # @overload result_constructor(sym_or_proc)
266
+ # Define how to build the +result+.
267
+ # @param sym_or_proc [Symbol, #call]
268
+ # - Either a +Symbol+ representing the _public_ method to call on the +result+ class.
269
+ # - Or anything that response to +#call+ (like a +Proc+).
270
+ # @return [#call] The callable constructor
271
+ #
272
+ # @example
273
+ # class MyOperation
274
+ # include Teckel::Operation
275
+ #
276
+ # class Result
277
+ # include Teckel::Result
278
+ # def initialize(value, success, opts = {}); end
279
+ # end
280
+ #
281
+ # # If you need more control over how to build a new +Result+ instance
282
+ # result_constructor ->(value, success) { result.new(value, success, {foo: :bar}) }
283
+ # end
284
+ def result_constructor(sym_or_proc = nil)
285
+ get_set_constructor(:result_constructor, result, sym_or_proc) ||
286
+ raise(MissingConfigError, "Missing result_constructor config for #{self}")
287
+ end
288
+
289
+ # @!group Shortcuts
290
+
291
+ # Shortcut to use {Teckel::Operation::Result} as a result object,
292
+ # wrapping any {error} or {output}.
293
+ #
294
+ # @!visibility protected
295
+ # @note Don't use in conjunction with {result} or {result_constructor}
296
+ # @return [nil]
297
+ def result!
298
+ @config.for(:result, Result)
299
+ @config.for(:result_constructor, Result.method(:new))
300
+ nil
301
+ end
302
+
303
+ # @!visibility private
304
+ REQUIRED_CONFIGS = %i[
305
+ input input_constructor
306
+ output output_constructor
307
+ error error_constructor
308
+ settings settings_constructor
309
+ result result_constructor
310
+ runner
311
+ ].freeze
312
+
313
+ # @!visibility private
314
+ # @return [void]
315
+ def define!
316
+ REQUIRED_CONFIGS.each { |e| public_send(e) }
317
+ nil
318
+ end
319
+
320
+ # Disallow any further changes to this Operation.
321
+ # Make sure all configurations are set.
322
+ #
323
+ # @raise [MissingConfigError]
324
+ # @return [self] Frozen self
325
+ # @!visibility public
326
+ def finalize!
327
+ define!
328
+ @config.freeze
329
+ self
330
+ end
331
+
332
+ # Produces a shallow copy of this operation and all it's configuration.
333
+ #
334
+ # @return [self]
335
+ # @!visibility public
336
+ def dup
337
+ dup_config(super())
338
+ end
339
+
340
+ # Produces a clone of this operation and all it's configuration
341
+ #
342
+ # @return [self]
343
+ # @!visibility public
344
+ def clone
345
+ if frozen?
346
+ super()
347
+ else
348
+ dup_config(super())
349
+ end
350
+ end
351
+
352
+ # Prevents further modifications to this operation and its config
353
+ #
354
+ # @return [self]
355
+ # @!visibility public
356
+ def freeze
357
+ @config.freeze
358
+ super()
359
+ end
360
+
361
+ # @!visibility private
362
+ def inherited(subclass)
363
+ super(dup_config(subclass))
364
+ end
365
+
366
+ # @!visibility private
367
+ def self.extended(base)
368
+ base.instance_exec do
369
+ @config = Teckel::Config.new
370
+ attr_accessor :runner
371
+ attr_accessor :settings
372
+ end
373
+ end
374
+
375
+ private
376
+
377
+ def dup_config(other_class)
378
+ other_class.instance_variable_set(:@config, @config.dup)
379
+ other_class
380
+ end
381
+
382
+ def get_set_constructor(name, on, sym_or_proc)
383
+ constructor = build_constructor(on, sym_or_proc)
384
+
385
+ @config.for(name, constructor) {
386
+ build_constructor(on, DEFAULT_CONSTRUCTOR)
387
+ }
388
+ end
389
+
390
+ def build_constructor(on, sym_or_proc)
391
+ case sym_or_proc
392
+ when Proc
393
+ sym_or_proc
394
+ when Symbol
395
+ on.public_method(sym_or_proc) if on.respond_to?(sym_or_proc)
396
+ end
397
+ end
398
+ end
399
+ end
400
+ end