teckel 0.1.0 → 0.2.0

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