teckel 0.4.0 → 0.5.0

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