teckel 0.4.0 → 0.8.0

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