teckel 0.1.0 → 0.2.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/config.rb CHANGED
@@ -2,10 +2,22 @@
2
2
 
3
3
  module Teckel
4
4
  class Config
5
- class FrozenConfigError < Teckel::Error; end
6
-
7
5
  @default_constructor = :[]
8
6
  class << self
7
+ # @!attribute [r] default_constructor()
8
+ # The default constructor method for +input+, +output+ and +error+ class (default: +:[]+)
9
+ # @return [Class] The Output class
10
+
11
+ # @!method default_constructor(sym_or_proc)
12
+ # Set the default constructor method for +input+, +output+ and +error+ class
13
+ #
14
+ # defaults to +:[]+
15
+ #
16
+ # @param sym_or_proc [Symbol,#call] The method name on the +input+,
17
+ # +output+ and +error+ class or a callable which accepts the
18
+ # +input+, +output+ or +error+
19
+ #
20
+ # @return [Symbol,#call]
9
21
  def default_constructor(sym_or_proc = nil)
10
22
  return @default_constructor if sym_or_proc.nil?
11
23
 
@@ -13,57 +25,37 @@ module Teckel
13
25
  end
14
26
  end
15
27
 
28
+ # @!visibility private
16
29
  def initialize
17
- @input_class = nil
18
- @input_constructor = nil
19
-
20
- @output_class = nil
21
- @output_constructor = nil
22
-
23
- @error_class = nil
24
- @error_constructor = nil
25
- end
26
-
27
- def input(klass = nil)
28
- return @input_class if klass.nil?
29
- raise FrozenConfigError unless @input_class.nil?
30
-
31
- @input_class = klass
32
- end
33
-
34
- def input_constructor(sym_or_proc = nil)
35
- return (@input_constructor || self.class.default_constructor) if sym_or_proc.nil?
36
- raise FrozenConfigError unless @input_constructor.nil?
37
-
38
- @input_constructor = sym_or_proc
39
- end
40
-
41
- def output(klass = nil)
42
- return @output_class if klass.nil?
43
- raise FrozenConfigError unless @output_class.nil?
44
-
45
- @output_class = klass
46
- end
47
-
48
- def output_constructor(sym_or_proc = nil)
49
- return (@output_constructor || self.class.default_constructor) if sym_or_proc.nil?
50
- raise FrozenConfigError unless @output_constructor.nil?
51
-
52
- @output_constructor = sym_or_proc
30
+ @config = {}
31
+ end
32
+
33
+ # @!visibility private
34
+ def for(key, value = nil, &block)
35
+ if value.nil?
36
+ if block
37
+ @config[key] ||= @config.fetch(key, &block)
38
+ else
39
+ @config[key]
40
+ end
41
+ elsif @config.key?(key)
42
+ raise FrozenConfigError, "Configuration #{key} is already set"
43
+ else
44
+ @config[key] = value
45
+ end
53
46
  end
54
47
 
55
- def error(klass = nil)
56
- return @error_class if klass.nil?
57
- raise FrozenConfigError unless @error_class.nil?
58
-
59
- @error_class = klass
48
+ # @!visibility private
49
+ def freeze
50
+ @config.freeze
51
+ super
60
52
  end
61
53
 
62
- def error_constructor(sym_or_proc = nil)
63
- return (@error_constructor || self.class.default_constructor) if sym_or_proc.nil?
64
- raise FrozenConfigError unless @error_constructor.nil?
65
-
66
- @error_constructor = sym_or_proc
54
+ # @!visibility private
55
+ def dup
56
+ super.tap do |copy|
57
+ copy.instance_variable_set(:@config, @config.dup)
58
+ end
67
59
  end
68
60
  end
69
61
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teckel
4
+ # Simple type object for enforcing +input+, +output+ or +error+ data to be
5
+ # not set (or +nil+)
6
+ class None
7
+ class << self
8
+ # Always return nil
9
+ # @return nil
10
+ # @raise [ArgumentError] when called with any non-nil arguments
11
+ def [](*args)
12
+ raise ArgumentError, "None called with arguments" if args.any?(&:itself)
13
+ end
14
+
15
+ alias :new :[]
16
+ end
17
+ end
18
+ end
@@ -9,10 +9,14 @@ module Teckel
9
9
  # the constants +Input+, +Output+ and +Error+, the second way is to use the
10
10
  # +input+. +output+ and +error+ methods to point them to anonymous classes.
11
11
  #
12
- # If you like "traditional" result objects (to ask +successful?+ or +failure?+ on)
13
- # see +Teckel::Operation::Results+
12
+ # If you like "traditional" result objects to ask +successful?+ or +failure?+ on,
13
+ # see {Teckel::Operation::Results Teckel::Operation::Results}
14
14
  #
15
- # @see Teckel::Operation::Results
15
+ # By default, +input+. +output+ and +error+ classes are build using +:[]+
16
+ # (eg: +Input[some: :param]+).
17
+ # Use {ClassMethods#input_constructor input_constructor},
18
+ # {ClassMethods#output_constructor output_constructor} and
19
+ # {ClassMethods#error_constructor error_constructor} to change them.
16
20
  #
17
21
  # @example class definitions via constants
18
22
  # class CreateUserViaConstants
@@ -38,13 +42,13 @@ module Teckel
38
42
  # error_constructor :new
39
43
  #
40
44
  # # @param [CreateUser::Input]
41
- # # @return [User | CreateUser::Error]
45
+ # # @return [User,CreateUser::Error]
42
46
  # def call(input)
43
47
  # user = ::User.new(name: input.name, age: input.age)
44
- # if user.safe
48
+ # if user.save
45
49
  # user
46
50
  # else
47
- # fail!(message: "Could not safe User", errors: user.errors)
51
+ # fail!(message: "Could not save User", errors: user.errors)
48
52
  # end
49
53
  # end
50
54
  # end
@@ -60,14 +64,13 @@ module Teckel
60
64
  # error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
61
65
  #
62
66
  # # @param [Hash<name: String, age: Integer>]
63
- # # @return [User | Hash<message: String, errors: [Hash]>]
67
+ # # @return [User,Hash<message: String, errors: [Hash]>]
64
68
  # def call(input)
65
69
  # user = User.new(name: input[:name], age: input[:age])
66
- # if user.safe
67
- # # exits early with success, prevents any further execution
68
- # success!(user)
70
+ # if user.save
71
+ # success!(user) # exits early with success, prevents any further execution
69
72
  # else
70
- # fail!(message: "Could not safe User", errors: user.errors)
73
+ # fail!(message: "Could not save User", errors: user.errors)
71
74
  # end
72
75
  # end
73
76
  # end
@@ -76,7 +79,7 @@ module Teckel
76
79
  # CreateUserViaMethods.call(name: "Bob", age: 23).is_a?(User) #=> true
77
80
  #
78
81
  # # A failure call:
79
- # CreateUserViaMethods.call(name: "Bob", age: 10).eql?(message: "Could not safe User", errors: [{age: "underage"}]) #=> true
82
+ # CreateUserViaMethods.call(name: "Bob", age: 10).eql?(message: "Could not save User", errors: [{age: "underage"}]) #=> true
80
83
  #
81
84
  # # Build your Input, Output and Error classes in a way that let you know:
82
85
  # begin; CreateUserViaMethods.call(unwanted: "input"); rescue => e; e end.is_a?(::Dry::Types::MissingKeyError) #=> true
@@ -84,8 +87,54 @@ module Teckel
84
87
  # # Feed an instance of the input class directly to call:
85
88
  # CreateUserViaMethods.call(CreateUserViaMethods.input[name: "Bob", age: 23]).is_a?(User) #=> true
86
89
  #
87
- # @api public
90
+ # @!visibility public
88
91
  module Operation
92
+ # The default implementation for executing a single {Operation}
93
+ #
94
+ # @!visibility protected
95
+ class Runner
96
+ # @!visibility private
97
+ UNDEFINED = Object.new.freeze
98
+
99
+ def initialize(operation)
100
+ @operation = operation
101
+ end
102
+ attr_reader :operation
103
+
104
+ def call(input)
105
+ err = catch(:failure) do
106
+ simple_return = UNDEFINED
107
+ out = catch(:success) do
108
+ simple_return = @operation.new.call(build_input(input))
109
+ end
110
+ return simple_return == UNDEFINED ? build_output(*out) : build_output(simple_return)
111
+ end
112
+ build_error(*err)
113
+ end
114
+
115
+ private
116
+
117
+ def build_input(input)
118
+ operation.input_constructor.call(input)
119
+ end
120
+
121
+ def build_output(*args)
122
+ if args.size == 1 && operation.output === args.first # rubocop:disable Style/CaseEquality
123
+ args.first
124
+ else
125
+ operation.output_constructor.call(*args)
126
+ end
127
+ end
128
+
129
+ def build_error(*args)
130
+ if args.size == 1 && operation.error === args.first # rubocop:disable Style/CaseEquality
131
+ args.first
132
+ else
133
+ operation.error_constructor.call(*args)
134
+ end
135
+ end
136
+ end
137
+
89
138
  module ClassMethods
90
139
  # @!attribute [r] input()
91
140
  # Get the configured class wrapping the input data structure.
@@ -96,11 +145,8 @@ module Teckel
96
145
  # @param klass [Class] The +input+ class
97
146
  # @return [Class] The +input+ class
98
147
  def input(klass = nil)
99
- return @input_class if @input_class
100
-
101
- @input_class = @config.input(klass)
102
- @input_class ||= self::Input if const_defined?(:Input)
103
- @input_class
148
+ @config.for(:input, klass) { self::Input if const_defined?(:Input) } ||
149
+ raise(Teckel::MissingConfigError, "Missing input config for #{self}")
104
150
  end
105
151
 
106
152
  # @!attribute [r] input_constructor()
@@ -109,9 +155,9 @@ module Teckel
109
155
 
110
156
  # @!method input_constructor(sym_or_proc)
111
157
  # Define how to build the +input+.
112
- # @param sym_or_proc [Symbol|#call]
158
+ # @param sym_or_proc [Symbol, #call]
113
159
  # - Either a +Symbol+ representing the _public_ method to call on the +input+ class.
114
- # - Or a callable (like a +Proc+).
160
+ # - Or anything that response to +#call+ (like a +Proc+).
115
161
  # @return [#call] The callable constructor
116
162
  #
117
163
  # @example simple symbol to method constructor
@@ -119,7 +165,7 @@ module Teckel
119
165
  # include Teckel::Operation
120
166
  #
121
167
  # class Input
122
- # # ...
168
+ # def initialize(name:, age:); end
123
169
  # end
124
170
  #
125
171
  # # If you need more control over how to build a new +Input+ instance
@@ -134,7 +180,7 @@ module Teckel
134
180
  # include Teckel::Operation
135
181
  #
136
182
  # class Input
137
- # # ...
183
+ # def initialize(*args, **opts); end
138
184
  # end
139
185
  #
140
186
  # # If you need more control over how to build a new +Input+ instance
@@ -143,16 +189,9 @@ module Teckel
143
189
  # end
144
190
  #
145
191
  # MyOperation.input_constructor.is_a?(Proc) #=> true
146
- def input_constructor(sym_or_proc = nil)
147
- return @input_constructor if @input_constructor
148
-
149
- constructor = @config.input_constructor(sym_or_proc)
150
- @input_constructor =
151
- if constructor.is_a?(Symbol) && input.respond_to?(constructor)
152
- input.public_method(constructor)
153
- elsif sym_or_proc.respond_to?(:call)
154
- sym_or_proc
155
- end
192
+ def input_constructor(sym_or_proc = Config.default_constructor)
193
+ @config.for(:input_constructor) { build_counstructor(input, sym_or_proc) } ||
194
+ raise(MissingConfigError, "Missing input_constructor config for #{self}")
156
195
  end
157
196
 
158
197
  # @!attribute [r] output()
@@ -164,11 +203,8 @@ module Teckel
164
203
  # @param klass [Class] The +output+ class
165
204
  # @return [Class] The +output+ class
166
205
  def output(klass = nil)
167
- return @output_class if @output_class
168
-
169
- @output_class = @config.output(klass)
170
- @output_class ||= self::Output if const_defined?(:Output)
171
- @output_class
206
+ @config.for(:output, klass) { self::Output if const_defined?(:Output) } ||
207
+ raise(Teckel::MissingConfigError, "Missing output config for #{self}")
172
208
  end
173
209
 
174
210
  # @!attribute [r] output_constructor()
@@ -177,9 +213,9 @@ module Teckel
177
213
 
178
214
  # @!method output_constructor(sym_or_proc)
179
215
  # Define how to build the +output+.
180
- # @param sym_or_proc [Symbol|#call]
216
+ # @param sym_or_proc [Symbol, #call]
181
217
  # - Either a +Symbol+ representing the _public_ method to call on the +output+ class.
182
- # - Or a callable (like a +Proc+).
218
+ # - Or anything that response to +#call+ (like a +Proc+).
183
219
  # @return [#call] The callable constructor
184
220
  #
185
221
  # @example
@@ -187,7 +223,7 @@ module Teckel
187
223
  # include Teckel::Operation
188
224
  #
189
225
  # class Output
190
- # # ....
226
+ # def initialize(*args, **opts); end
191
227
  # end
192
228
  #
193
229
  # # MyOperation.call("foo", "bar") # -> Output.new("foo", "bar")
@@ -197,16 +233,9 @@ module Teckel
197
233
  # # MyOperation.call("foo", opt: "bar") # -> Output.new(name: "foo", opt: "bar")
198
234
  # output_constructor ->(name, options) { Output.new(name: name, **options) }
199
235
  # end
200
- def output_constructor(sym_or_proc = nil)
201
- return @output_constructor if @output_constructor
202
-
203
- constructor = @config.output_constructor(sym_or_proc)
204
- @output_constructor =
205
- if constructor.is_a?(Symbol) && output.respond_to?(constructor)
206
- output.public_method(constructor)
207
- elsif sym_or_proc.respond_to?(:call)
208
- sym_or_proc
209
- end
236
+ def output_constructor(sym_or_proc = Config.default_constructor)
237
+ @config.for(:output_constructor) { build_counstructor(output, sym_or_proc) } ||
238
+ raise(MissingConfigError, "Missing output_constructor config for #{self}")
210
239
  end
211
240
 
212
241
  # @!attribute [r] error()
@@ -216,13 +245,10 @@ module Teckel
216
245
  # @!method error(klass)
217
246
  # Set the class wrapping the error data structure.
218
247
  # @param klass [Class] The +error+ class
219
- # @return [Class] The +error+ class
248
+ # @return [Class,nil] The +error+ class or +nil+ if it does not error
220
249
  def error(klass = nil)
221
- return @error_class if @error_class
222
-
223
- @error_class = @config.error(klass)
224
- @error_class ||= self::Error if const_defined?(:Error)
225
- @error_class
250
+ @config.for(:error, klass) { self::Error if const_defined?(:Error) } ||
251
+ raise(Teckel::MissingConfigError, "Missing error config for #{self}")
226
252
  end
227
253
 
228
254
  # @!attribute [r] error_constructor()
@@ -231,9 +257,9 @@ module Teckel
231
257
 
232
258
  # @!method error_constructor(sym_or_proc)
233
259
  # Define how to build the +error+.
234
- # @param sym_or_proc [Symbol|#call]
260
+ # @param sym_or_proc [Symbol, #call]
235
261
  # - Either a +Symbol+ representing the _public_ method to call on the +error+ class.
236
- # - Or a callable (like a +Proc+).
262
+ # - Or anything that response to +#call+ (like a +Proc+).
237
263
  # @return [#call] The callable constructor
238
264
  #
239
265
  # @example
@@ -241,7 +267,7 @@ module Teckel
241
267
  # include Teckel::Operation
242
268
  #
243
269
  # class Error
244
- # # ....
270
+ # def initialize(*args, **opts); end
245
271
  # end
246
272
  #
247
273
  # # MyOperation.call("foo", "bar") # -> Error.new("foo", "bar")
@@ -251,72 +277,126 @@ module Teckel
251
277
  # # MyOperation.call("foo", opt: "bar") # -> Error.new(name: "foo", opt: "bar")
252
278
  # error_constructor ->(name, options) { Error.new(name: name, **options) }
253
279
  # end
254
- def error_constructor(sym_or_proc = nil)
255
- return @error_constructor if @error_constructor
256
-
257
- constructor = @config.error_constructor(sym_or_proc)
258
- @error_constructor =
259
- if constructor.is_a?(Symbol) && error.respond_to?(constructor)
260
- error.public_method(constructor)
261
- elsif sym_or_proc.respond_to?(:call)
262
- sym_or_proc
263
- end
280
+ def error_constructor(sym_or_proc = Config.default_constructor)
281
+ @config.for(:error_constructor) { build_counstructor(error, sym_or_proc) } ||
282
+ raise(MissingConfigError, "Missing error_constructor config for #{self}")
264
283
  end
265
284
 
266
- # Invoke the Operation
285
+ # Convenience method for setting {#input}, {#output} or {#error} to the {None} value.
286
+ # @return [None]
287
+ # @example Enforcing nil input, output or error
288
+ # class MyOperation
289
+ # include Teckel::Operation
267
290
  #
268
- # @param input Any form of input your +input+ class can handle via the given +input_constructor+
269
- # @return Either An instance of your defined +error+ class or +output+ class
270
- def call(input)
271
- new.call!(input)
291
+ # input none
292
+ #
293
+ # # same as
294
+ # output Teckel::None
295
+ #
296
+ # error none
297
+ #
298
+ # def call(_) # you still need to take than +nil+ input when using `input none`
299
+ # # when using `error none`:
300
+ # # `fail!` works, but `fail!("data")` raises an error
301
+ #
302
+ # # when using `output none`:
303
+ # # `success!` works, but `success!("data")` raises an error
304
+ # # same thing when using simple return values as success:
305
+ # # take care to not return anything
306
+ # nil
307
+ # end
308
+ # end
309
+ #
310
+ # MyOperation.call #=> nil
311
+ def none
312
+ None
272
313
  end
273
- end
274
314
 
275
- module InstanceMethods
315
+ # @!attribute [r] runner()
316
+ # @return [Class] The Runner class
276
317
  # @!visibility protected
277
- def call!(input)
278
- catch(:failure) do
279
- out = catch(:success) do
280
- simple_ret = call(build_input(input))
281
- build_output(simple_ret)
282
- end
283
- return out
284
- end
285
- end
286
318
 
319
+ # Overwrite the default runner
320
+ # @param klass [Class] A class like the {Runner}
287
321
  # @!visibility protected
288
- def success!(*args)
289
- throw :success, build_output(*args)
322
+ def runner(klass = nil)
323
+ @config.for(:runner, klass) { Runner }
290
324
  end
291
325
 
292
- # @!visibility protected
293
- def fail!(*args)
294
- throw :failure, build_error(*args)
326
+ # Invoke the Operation
327
+ #
328
+ # @param input Any form of input your +input+ class can handle via the given +input_constructor+
329
+ # @return Either An instance of your defined +error+ class or +output+ class
330
+ # @!visibility public
331
+ def call(input = nil)
332
+ runner.new(self).call(input)
295
333
  end
296
334
 
297
- private
335
+ # @!visibility private
336
+ # @return [nil]
337
+ def define!
338
+ %i[input input_constructor output output_constructor error error_constructor runner].each { |e|
339
+ public_send(e)
340
+ }
341
+ nil
342
+ end
298
343
 
299
- def build_input(input)
300
- self.class.input_constructor.call(input)
344
+ # Disallow any further changes to this Operation.
345
+ # Make sure all configurations are set.
346
+ #
347
+ # @raise [MissingConfigError]
348
+ # @return [self] Frozen self
349
+ # @!visibility public
350
+ def finalize!
351
+ define!
352
+ freeze
353
+ @config.freeze
354
+ self
301
355
  end
302
356
 
303
- def build_output(*args)
304
- if args.size == 1 && self.class.output === args.first # rubocop:disable Style/CaseEquality
305
- args.first
306
- else
307
- self.class.output_constructor.call(*args)
357
+ # @!visibility public
358
+ def dup
359
+ super.tap do |copy|
360
+ copy.instance_variable_set(:@config, @config.dup)
308
361
  end
309
362
  end
310
363
 
311
- def build_error(*args)
312
- if args.size == 1 && self.class.error === args.first # rubocop:disable Style/CaseEquality
313
- args.first
364
+ # @!visibility public
365
+ def clone
366
+ if frozen?
367
+ super
314
368
  else
315
- self.class.error_constructor.call(*args)
369
+ super.tap do |copy|
370
+ copy.instance_variable_set(:@config, @config.dup)
371
+ end
372
+ end
373
+ end
374
+
375
+ private
376
+
377
+ def build_counstructor(on, sym_or_proc)
378
+ if sym_or_proc.is_a?(Symbol) && on.respond_to?(sym_or_proc)
379
+ on.public_method(sym_or_proc)
380
+ elsif sym_or_proc.respond_to?(:call)
381
+ sym_or_proc
316
382
  end
317
383
  end
318
384
  end
319
385
 
386
+ module InstanceMethods
387
+ # Halt any further execution with a +output+ value
388
+ # @!visibility protected
389
+ def success!(*args)
390
+ throw :success, args
391
+ end
392
+
393
+ # Halt any further execution with an +error+ value
394
+ # @!visibility protected
395
+ def fail!(*args)
396
+ throw :failure, args
397
+ end
398
+ end
399
+
320
400
  def self.included(receiver)
321
401
  receiver.extend ClassMethods
322
402
  receiver.send :include, InstanceMethods
@@ -324,14 +404,7 @@ module Teckel
324
404
  receiver.class_eval do
325
405
  @config = Config.new
326
406
 
327
- @input_class = nil
328
- @input_constructor = nil
329
-
330
- @output_class = nil
331
- @output_constructor = nil
332
-
333
- @error_class = nil
334
- @error_constructor = nil
407
+ @runner = Runner
335
408
 
336
409
  protected :success!, :fail!
337
410
  end