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.
@@ -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 == 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
@@ -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
@@ -48,5 +44,15 @@ module Teckel
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
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "operation/config"
3
4
  require_relative "operation/result"
4
5
  require_relative "operation/runner"
5
6
 
@@ -13,13 +14,14 @@ module Teckel
13
14
  # +input+. +output+ and +error+ methods to point them to anonymous classes.
14
15
  #
15
16
  # If you like "traditional" result objects to ask +successful?+ or +failure?+ on,
16
- # use {.result!} and get {Teckel::Operation::Result}
17
+ # use {Teckel::Operation::Config#result! result!} and get {Teckel::Operation::Result}
17
18
  #
18
19
  # By default, +input+. +output+ and +error+ classes are build using +:[]+
19
20
  # (eg: +Input[some: :param]+).
20
- # Use {ClassMethods#input_constructor input_constructor},
21
- # {ClassMethods#output_constructor output_constructor} and
22
- # {ClassMethods#error_constructor error_constructor} to change them.
21
+ #
22
+ # Use {Teckel::Operation::Config#input_constructor input_constructor},
23
+ # {Teckel::Operation::Config#output_constructor output_constructor} and
24
+ # {Teckel::Operation::Config#error_constructor error_constructor} to change them.
23
25
  #
24
26
  # @example class definitions via methods
25
27
  # class CreateUserViaMethods
@@ -56,309 +58,21 @@ module Teckel
56
58
  # @!visibility public
57
59
  module Operation
58
60
  module ClassMethods
59
- # @!group Contacts definition
60
-
61
- # @overload input()
62
- # Get the configured class wrapping the input data structure.
63
- # @return [Class] The +input+ class
64
- # @overload input(klass)
65
- # Set the class wrapping the input data structure.
66
- # @param klass [Class] The +input+ class
67
- # @return [Class] The +input+ class
68
- def input(klass = nil)
69
- @config.for(:input, klass) { self::Input if const_defined?(:Input) } ||
70
- raise(Teckel::MissingConfigError, "Missing input config for #{self}")
71
- end
72
-
73
- # @overload input_constructor()
74
- # The callable constructor to build an instance of the +input+ class.
75
- # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
76
- # @return [Proc] A callable that will return an instance of the +input+ class.
77
- #
78
- # @overload input_constructor(sym_or_proc)
79
- # Define how to build the +input+.
80
- # @param sym_or_proc [Symbol, #call]
81
- # - Either a +Symbol+ representing the _public_ method to call on the +input+ class.
82
- # - Or anything that response to +#call+ (like a +Proc+).
83
- # @return [#call] The callable constructor
84
- #
85
- # @example simple symbol to method constructor
86
- # class MyOperation
87
- # include Teckel::Operation
88
- #
89
- # class Input
90
- # def initialize(name:, age:); end
91
- # end
92
- #
93
- # # If you need more control over how to build a new +Input+ instance
94
- # # MyOperation.call(name: "Bob", age: 23) # -> Input.new(name: "Bob", age: 23)
95
- # input_constructor :new
96
- # end
97
- #
98
- # MyOperation.input_constructor.is_a?(Method) #=> true
99
- #
100
- # @example Custom Proc constructor
101
- # class MyOperation
102
- # include Teckel::Operation
103
- #
104
- # class Input
105
- # def initialize(*args, **opts); end
106
- # end
107
- #
108
- # # If you need more control over how to build a new +Input+ instance
109
- # # MyOperation.call("foo", opt: "bar") # -> Input.new(name: "foo", opt: "bar")
110
- # input_constructor ->(name, options) { Input.new(name: name, **options) }
111
- # end
112
- #
113
- # MyOperation.input_constructor.is_a?(Proc) #=> true
114
- def input_constructor(sym_or_proc = nil)
115
- get_set_counstructor(:input_constructor, input, sym_or_proc) ||
116
- raise(MissingConfigError, "Missing input_constructor config for #{self}")
117
- end
118
-
119
- # @overload output()
120
- # Get the configured class wrapping the output data structure.
121
- # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
122
- # @return [Class] The +output+ class
123
- #
124
- # @overload output(klass)
125
- # Set the class wrapping the output data structure.
126
- # @param klass [Class] The +output+ class
127
- # @return [Class] The +output+ class
128
- def output(klass = nil)
129
- @config.for(:output, klass) { self::Output if const_defined?(:Output) } ||
130
- raise(Teckel::MissingConfigError, "Missing output config for #{self}")
131
- end
132
-
133
- # @overload output_constructor()
134
- # The callable constructor to build an instance of the +output+ class.
135
- # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
136
- # @return [Proc] A callable that will return an instance of +output+ class.
137
- #
138
- # @overload output_constructor(sym_or_proc)
139
- # Define how to build the +output+.
140
- # @param sym_or_proc [Symbol, #call]
141
- # - Either a +Symbol+ representing the _public_ method to call on the +output+ class.
142
- # - Or anything that response to +#call+ (like a +Proc+).
143
- # @return [#call] The callable constructor
144
- #
145
- # @example
146
- # class MyOperation
147
- # include Teckel::Operation
148
- #
149
- # class Output
150
- # def initialize(*args, **opts); end
151
- # end
152
- #
153
- # # MyOperation.call("foo", "bar") # -> Output.new("foo", "bar")
154
- # output_constructor :new
155
- #
156
- # # If you need more control over how to build a new +Output+ instance
157
- # # MyOperation.call("foo", opt: "bar") # -> Output.new(name: "foo", opt: "bar")
158
- # output_constructor ->(name, options) { Output.new(name: name, **options) }
159
- # end
160
- def output_constructor(sym_or_proc = nil)
161
- get_set_counstructor(:output_constructor, output, sym_or_proc) ||
162
- raise(MissingConfigError, "Missing output_constructor config for #{self}")
163
- end
164
-
165
- # @overload error()
166
- # Get the configured class wrapping the error data structure.
167
- # @return [Class] The +error+ class
168
- #
169
- # @overload error(klass)
170
- # Set the class wrapping the error data structure.
171
- # @param klass [Class] The +error+ class
172
- # @return [Class,nil] The +error+ class or +nil+ if it does not error
173
- def error(klass = nil)
174
- @config.for(:error, klass) { self::Error if const_defined?(:Error) } ||
175
- raise(Teckel::MissingConfigError, "Missing error config for #{self}")
176
- end
177
-
178
- # @overload error_constructor()
179
- # The callable constructor to build an instance of the +error+ class.
180
- # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
181
- # @return [Proc] A callable that will return an instance of +error+ class.
182
- #
183
- # @overload error_constructor(sym_or_proc)
184
- # Define how to build the +error+.
185
- # @param sym_or_proc [Symbol, #call]
186
- # - Either a +Symbol+ representing the _public_ method to call on the +error+ class.
187
- # - Or anything that response to +#call+ (like a +Proc+).
188
- # @return [#call] The callable constructor
189
- #
190
- # @example
191
- # class MyOperation
192
- # include Teckel::Operation
193
- #
194
- # class Error
195
- # def initialize(*args, **opts); end
196
- # end
197
- #
198
- # # MyOperation.call("foo", "bar") # -> Error.new("foo", "bar")
199
- # error_constructor :new
200
- #
201
- # # If you need more control over how to build a new +Error+ instance
202
- # # MyOperation.call("foo", opt: "bar") # -> Error.new(name: "foo", opt: "bar")
203
- # error_constructor ->(name, options) { Error.new(name: name, **options) }
204
- # end
205
- def error_constructor(sym_or_proc = nil)
206
- get_set_counstructor(:error_constructor, error, sym_or_proc) ||
207
- raise(MissingConfigError, "Missing error_constructor config for #{self}")
208
- end
209
-
210
- # @!endgroup
211
-
212
- # @overload settings()
213
- # Get the configured class wrapping the settings data structure.
214
- # @return [Class] The +settings+ class, or {Teckel::Contracts::None} as default
215
- #
216
- # @overload settings(klass)
217
- # Set the class wrapping the settings data structure.
218
- # @param klass [Class] The +settings+ class
219
- # @return [Class] The +settings+ class configured
220
- def settings(klass = nil)
221
- @config.for(:settings, klass) { const_defined?(:Settings) ? self::Settings : none }
222
- end
223
-
224
- # @overload settings_constructor()
225
- # The callable constructor to build an instance of the +settings+ class.
226
- # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
227
- # @return [Proc] A callable that will return an instance of +settings+ class.
228
- #
229
- # @overload settings_constructor(sym_or_proc)
230
- # Define how to build the +settings+.
231
- # @param sym_or_proc [Symbol, #call]
232
- # - Either a +Symbol+ representing the _public_ method to call on the +settings+ class.
233
- # - Or anything that response to +#call+ (like a +Proc+).
234
- # @return [#call] The callable constructor
235
- #
236
- # @example
237
- # class MyOperation
238
- # include Teckel::Operation
239
- #
240
- # class Settings
241
- # def initialize(*args); end
242
- # end
243
- #
244
- # # MyOperation.with("foo", "bar") # -> Settings.new("foo", "bar")
245
- # settings_constructor :new
246
- # end
247
- def settings_constructor(sym_or_proc = nil)
248
- get_set_counstructor(:settings_constructor, settings, sym_or_proc) ||
249
- raise(MissingConfigError, "Missing settings_constructor config for #{self}")
250
- end
251
-
252
- # @overload runner()
253
- # @return [Class] The Runner class
254
- # @!visibility protected
255
- #
256
- # @overload runner(klass)
257
- # Overwrite the default runner
258
- # @param klass [Class] A class like the {Runner}
259
- # @!visibility protected
260
- def runner(klass = nil)
261
- @config.for(:runner, klass) { Teckel::Operation::Runner }
262
- end
263
-
264
- # @overload result()
265
- # Get the configured result object class wrapping {error} or {output}.
266
- # The {ValueResult} default will act as a pass-through and does. Any error
267
- # or output will just returned as-is.
268
- # @return [Class] The +result+ class, or {ValueResult} as default
269
- #
270
- # @overload result(klass)
271
- # Set the result object class wrapping {error} or {output}.
272
- # @param klass [Class] The +result+ class
273
- # @return [Class] The +result+ class configured
274
- def result(klass = nil)
275
- @config.for(:result, klass) { const_defined?(:Result, false) ? self::Result : ValueResult }
276
- end
277
-
278
- # @overload result_constructor()
279
- # The callable constructor to build an instance of the +result+ class.
280
- # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
281
- # @return [Proc] A callable that will return an instance of +result+ class.
282
- #
283
- # @overload result_constructor(sym_or_proc)
284
- # Define how to build the +result+.
285
- # @param sym_or_proc [Symbol, #call]
286
- # - Either a +Symbol+ representing the _public_ method to call on the +result+ class.
287
- # - Or anything that response to +#call+ (like a +Proc+).
288
- # @return [#call] The callable constructor
289
- #
290
- # @example
291
- # class MyOperation
292
- # include Teckel::Operation
293
- #
294
- # class Result
295
- # include Teckel::Result
296
- # def initialize(value, success, opts = {}); end
297
- # end
298
- #
299
- # # If you need more control over how to build a new +Settings+ instance
300
- # result_constructor ->(value, success) { result.new(value, success, {foo: :bar}) }
301
- # end
302
- def result_constructor(sym_or_proc = nil)
303
- get_set_counstructor(:result_constructor, result, sym_or_proc) ||
304
- raise(MissingConfigError, "Missing result_constructor config for #{self}")
305
- end
306
-
307
- # @!group Shortcuts
308
-
309
- # Shortcut to use {Teckel::Operation::Result} as a result object,
310
- # wrapping any {error} or {output}.
311
- #
312
- # @!visibility protected
313
- # @note Don't use in conjunction with {result} or {result_constructor}
314
- # @return [nil]
315
- def result!
316
- @config.for(:result, Teckel::Operation::Result)
317
- @config.for(:result_constructor, Teckel::Operation::Result.method(:new))
318
- nil
319
- end
320
-
321
- # Convenience method for setting {#input}, {#output} or {#error} to the
322
- # {Teckel::Contracts::None} value.
323
- # @return [Object] The {Teckel::Contracts::None} class.
324
- #
325
- # @example Enforcing nil input, output or error
326
- # class MyOperation
327
- # include Teckel::Operation
328
- #
329
- # input none
330
- #
331
- # # same as
332
- # output Teckel::Contracts::None
333
- #
334
- # error none
335
- #
336
- # def call(_) # you still need to take than +nil+ input when using `input none`
337
- # # when using `error none`:
338
- # # `fail!` works, but `fail!("data")` raises an error
339
- #
340
- # # when using `output none`:
341
- # # `success!` works, but `success!("data")` raises an error
342
- # # same thing when using simple return values as success:
343
- # # take care to not return anything
344
- # nil
345
- # end
346
- # end
347
- #
348
- # MyOperation.call #=> nil
349
- def none
350
- Teckel::Contracts::None
351
- end
352
-
353
- # @endgroup
354
-
355
61
  # Invoke the Operation
356
62
  #
357
- # @param input Any form of input your {#input} class can handle via the given {#input_constructor}
358
- # @return Either An instance of your defined {#error} class or {#output} class
63
+ # @param input Any form of input your {Teckel::Operation::Config#input input} class can handle via the given
64
+ # {Teckel::Operation::Config#input_constructor input_constructor}
65
+ # @return Either An instance of your defined {Teckel::Operation::Config#error error} class or
66
+ # {Teckel::Operation::Config#output output} class
359
67
  # @!visibility public
360
68
  def call(input = nil)
361
- runner.new(self).call(input)
69
+ default_settings = self.default_settings
70
+
71
+ if default_settings
72
+ runner.new(self, default_settings.call)
73
+ else
74
+ runner.new(self)
75
+ end.call(input)
362
76
  end
363
77
 
364
78
  # Provide {InstanceMethods#settings() settings} to the running operation.
@@ -366,8 +80,9 @@ module Teckel
366
80
  # This method is intended to be called on the operation class outside of
367
81
  # it's definition, prior to running {#call}.
368
82
  #
369
- # @param input Any form of input your {#settings} class can handle via the given {#settings_constructor}
370
- # @return [Class] The configured {runner}
83
+ # @param input Any form of input your {Teckel::Operation::Config#settings settings} class can handle via the given
84
+ # {Teckel::Operation::Config#settings_constructor settings_constructor}
85
+ # @return [Class] The configured {Teckel::Operation::Config#runner runner}
371
86
  # @!visibility public
372
87
  #
373
88
  # @example Inject settings for an operation call
@@ -401,85 +116,39 @@ module Teckel
401
116
  end
402
117
  alias :set :with
403
118
 
404
- # @!visibility private
405
- # @return [void]
406
- def define!
407
- %i[
408
- input input_constructor
409
- output output_constructor
410
- error error_constructor
411
- settings settings_constructor
412
- result result_constructor
413
- runner
414
- ].each { |e| public_send(e) }
415
- nil
416
- end
417
-
418
- # Disallow any further changes to this Operation.
419
- # Make sure all configurations are set.
119
+ # Convenience method for setting {Teckel::Operation::Config#input input},
120
+ # {Teckel::Operation::Config#output output} or
121
+ # {Teckel::Operation::Config#error error} to the
122
+ # {Teckel::Contracts::None} value.
420
123
  #
421
- # @raise [MissingConfigError]
422
- # @return [self] Frozen self
423
- # @!visibility public
424
- def finalize!
425
- define!
426
- @config.freeze
427
- self
428
- end
429
-
430
- # Produces a shallow copy of this operation and all it's configuration.
124
+ # @return [Object] The {Teckel::Contracts::None} class.
431
125
  #
432
- # @return [self]
433
- # @!visibility public
434
- def dup
435
- super.tap do |copy|
436
- copy.instance_variable_set(:@config, @config.dup)
437
- end
438
- end
439
-
440
- # Produces a clone of this operation and all it's configuration
126
+ # @example Enforcing nil input, output or error
127
+ # class MyOperation
128
+ # include Teckel::Operation
441
129
  #
442
- # @return [self]
443
- # @!visibility public
444
- def clone
445
- if frozen?
446
- super
447
- else
448
- super.tap do |copy|
449
- copy.instance_variable_set(:@config, @config.dup)
450
- end
451
- end
452
- end
453
-
454
- # @!visibility private
455
- def inherited(subclass)
456
- subclass.instance_variable_set(:@config, @config.dup)
457
- end
458
-
459
- # @!visibility private
460
- def self.extended(base)
461
- base.instance_exec do
462
- @config = Config.new
463
- attr_accessor :settings
464
- end
465
- end
466
-
467
- private
468
-
469
- def get_set_counstructor(name, on, sym_or_proc)
470
- constructor = build_counstructor(on, sym_or_proc) unless sym_or_proc.nil?
471
-
472
- @config.for(name, constructor) {
473
- build_counstructor(on, Teckel::DEFAULT_CONSTRUCTOR)
474
- }
475
- end
476
-
477
- def build_counstructor(on, sym_or_proc)
478
- if sym_or_proc.is_a?(Symbol) && on.respond_to?(sym_or_proc)
479
- on.public_method(sym_or_proc)
480
- elsif sym_or_proc.respond_to?(:call)
481
- sym_or_proc
482
- end
130
+ # input none
131
+ #
132
+ # # same as
133
+ # output Teckel::Contracts::None
134
+ #
135
+ # error none
136
+ #
137
+ # def call(_) # you still need to take than +nil+ input when using `input none`
138
+ # # when using `error none`:
139
+ # # `fail!` works, but `fail!("data")` raises an error
140
+ #
141
+ # # when using `output none`:
142
+ # # `success!` works, but `success!("data")` raises an error
143
+ # # same thing when using simple return values as success:
144
+ # # take care to not return anything
145
+ # nil
146
+ # end
147
+ # end
148
+ #
149
+ # MyOperation.call #=> nil
150
+ def none
151
+ Teckel::Contracts::None
483
152
  end
484
153
  end
485
154
 
@@ -492,7 +161,7 @@ module Teckel
492
161
 
493
162
  # Halt any further execution with a output value
494
163
  #
495
- # @return a thing matching your {Operation::ClassMethods#output Operation#output} definition
164
+ # @return a thing matching your {Teckel::Operation::Config#output output} definition
496
165
  # @!visibility protected
497
166
  def success!(*args)
498
167
  throw :success, args
@@ -500,7 +169,7 @@ module Teckel
500
169
 
501
170
  # Halt any further execution with an error value
502
171
  #
503
- # @return a thing matching your {Operation::ClassMethods#error Operation#error} definition
172
+ # @return a thing matching your {Teckel::Operation::Config#error error} definition
504
173
  # @!visibility protected
505
174
  def fail!(*args)
506
175
  throw :failure, args
@@ -508,6 +177,7 @@ module Teckel
508
177
  end
509
178
 
510
179
  def self.included(receiver)
180
+ receiver.extend Config
511
181
  receiver.extend ClassMethods
512
182
  receiver.send :include, InstanceMethods
513
183
  end