teckel 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,394 @@
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(Teckel::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
+ raise(MissingConfigError, "Missing input_constructor config for #{self}")
62
+ end
63
+
64
+ # @overload output()
65
+ # Get the configured class wrapping the output data structure.
66
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
67
+ # @return [Class] The +output+ class
68
+ #
69
+ # @overload output(klass)
70
+ # Set the class wrapping the output data structure.
71
+ # @param klass [Class] The +output+ class
72
+ # @return [Class] The +output+ class
73
+ def output(klass = nil)
74
+ @config.for(:output, klass) { self::Output if const_defined?(:Output) } ||
75
+ raise(Teckel::MissingConfigError, "Missing output config for #{self}")
76
+ end
77
+
78
+ # @overload output_constructor()
79
+ # The callable constructor to build an instance of the +output+ class.
80
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
81
+ # @return [Proc] A callable that will return an instance of +output+ class.
82
+ #
83
+ # @overload output_constructor(sym_or_proc)
84
+ # Define how to build the +output+.
85
+ # @param sym_or_proc [Symbol, #call]
86
+ # - Either a +Symbol+ representing the _public_ method to call on the +output+ class.
87
+ # - Or anything that response to +#call+ (like a +Proc+).
88
+ # @return [#call] The callable constructor
89
+ #
90
+ # @example
91
+ # class MyOperation
92
+ # include Teckel::Operation
93
+ #
94
+ # class Output
95
+ # def initialize(*args, **opts); end
96
+ # end
97
+ #
98
+ # # MyOperation.call("foo", "bar") # -> Output.new("foo", "bar")
99
+ # output_constructor :new
100
+ #
101
+ # # If you need more control over how to build a new +Output+ instance
102
+ # # MyOperation.call("foo", opt: "bar") # -> Output.new(name: "foo", opt: "bar")
103
+ # output_constructor ->(name, options) { Output.new(name: name, **options) }
104
+ # end
105
+ def output_constructor(sym_or_proc = nil)
106
+ get_set_constructor(:output_constructor, output, sym_or_proc) ||
107
+ raise(MissingConfigError, "Missing output_constructor config for #{self}")
108
+ end
109
+
110
+ # @overload error()
111
+ # Get the configured class wrapping the error data structure.
112
+ # @return [Class] The +error+ class
113
+ #
114
+ # @overload error(klass)
115
+ # Set the class wrapping the error data structure.
116
+ # @param klass [Class] The +error+ class
117
+ # @return [Class,nil] The +error+ class or +nil+ if it does not error
118
+ def error(klass = nil)
119
+ @config.for(:error, klass) { self::Error if const_defined?(:Error) } ||
120
+ raise(Teckel::MissingConfigError, "Missing error config for #{self}")
121
+ end
122
+
123
+ # @overload error_constructor()
124
+ # The callable constructor to build an instance of the +error+ class.
125
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
126
+ # @return [Proc] A callable that will return an instance of +error+ class.
127
+ #
128
+ # @overload error_constructor(sym_or_proc)
129
+ # Define how to build the +error+.
130
+ # @param sym_or_proc [Symbol, #call]
131
+ # - Either a +Symbol+ representing the _public_ method to call on the +error+ class.
132
+ # - Or anything that response to +#call+ (like a +Proc+).
133
+ # @return [#call] The callable constructor
134
+ #
135
+ # @example
136
+ # class MyOperation
137
+ # include Teckel::Operation
138
+ #
139
+ # class Error
140
+ # def initialize(*args, **opts); end
141
+ # end
142
+ #
143
+ # # MyOperation.call("foo", "bar") # -> Error.new("foo", "bar")
144
+ # error_constructor :new
145
+ #
146
+ # # If you need more control over how to build a new +Error+ instance
147
+ # # MyOperation.call("foo", opt: "bar") # -> Error.new(name: "foo", opt: "bar")
148
+ # error_constructor ->(name, options) { Error.new(name: name, **options) }
149
+ # end
150
+ def error_constructor(sym_or_proc = nil)
151
+ get_set_constructor(:error_constructor, error, sym_or_proc) ||
152
+ raise(MissingConfigError, "Missing error_constructor config for #{self}")
153
+ end
154
+
155
+ # @!endgroup
156
+
157
+ # @overload settings()
158
+ # Get the configured class wrapping the settings data structure.
159
+ # @return [Class] The +settings+ class, or {Teckel::Contracts::None} as default
160
+ #
161
+ # @overload settings(klass)
162
+ # Set the class wrapping the settings data structure.
163
+ # @param klass [Class] The +settings+ class
164
+ # @return [Class] The +settings+ class configured
165
+ def settings(klass = nil)
166
+ @config.for(:settings, klass) { const_defined?(:Settings) ? self::Settings : none }
167
+ end
168
+
169
+ # @overload settings_constructor()
170
+ # The callable constructor to build an instance of the +settings+ class.
171
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
172
+ # @return [Proc] A callable that will return an instance of +settings+ class.
173
+ #
174
+ # @overload settings_constructor(sym_or_proc)
175
+ # Define how to build the +settings+.
176
+ # @param sym_or_proc [Symbol, #call]
177
+ # - Either a +Symbol+ representing the _public_ method to call on the +settings+ class.
178
+ # - Or anything that response to +#call+ (like a +Proc+).
179
+ # @return [#call] The callable constructor
180
+ #
181
+ # @example
182
+ # class MyOperation
183
+ # include Teckel::Operation
184
+ #
185
+ # class Settings
186
+ # def initialize(*args); end
187
+ # end
188
+ #
189
+ # # MyOperation.with("foo", "bar") # -> Settings.new("foo", "bar")
190
+ # settings_constructor :new
191
+ # end
192
+ def settings_constructor(sym_or_proc = nil)
193
+ get_set_constructor(:settings_constructor, settings, sym_or_proc) ||
194
+ raise(MissingConfigError, "Missing settings_constructor config for #{self}")
195
+ end
196
+
197
+ # Declare default settings this operation should use when called without
198
+ # {Teckel::Operation::ClassMethods#with #with}.
199
+ # When executing a Operation, +settings+ will no longer be +nil+, but
200
+ # whatever you define here.
201
+ #
202
+ # Explicit call-time settings will *not* get merged with declared default setting.
203
+ #
204
+ # @overload default_settings!()
205
+ # When this operation is called without {Teckel::Operation::ClassMethods#with #with},
206
+ # +settings+ will be an instance of the +settings+ class, initialized with no arguments.
207
+ #
208
+ # @overload default_settings!(sym_or_proc)
209
+ # When this operation is called without {Teckel::Operation::ClassMethods#with #with},
210
+ # +settings+ will be an instance of this callable constructor.
211
+ #
212
+ # @param sym_or_proc [Symbol, #call]
213
+ # - Either a +Symbol+ representing the _public_ method to call on the +settings+ class.
214
+ # - Or anything that responds to +#call+ (like a +Proc+).
215
+ #
216
+ # @overload default_settings!(arg1, arg2, ...)
217
+ # When this operation is called without {Teckel::Operation::ClassMethods#with #with},
218
+ # +settings+ will be an instance of the +settings+ class, initialized with those arguments.
219
+ #
220
+ # (Like calling +MyOperation.with(arg1, arg2, ...)+)
221
+ def default_settings!(*args)
222
+ callable =
223
+ if args.empty?
224
+ -> { settings_constructor.call }
225
+ elsif args.length == 1
226
+ build_constructor(settings, args.first)
227
+ end
228
+
229
+ callable ||= -> { settings_constructor.call(*args) }
230
+
231
+ @config.for(:default_settings, callable)
232
+ end
233
+
234
+ # Getter for configured default settings
235
+ # @return [nil|#call] The callable constructor
236
+ def default_settings
237
+ @config.for(:default_settings)
238
+ end
239
+
240
+ # @overload runner()
241
+ # @return [Class] The Runner class
242
+ # @!visibility protected
243
+ #
244
+ # @overload runner(klass)
245
+ # Overwrite the default runner
246
+ # @param klass [Class] A class like the {Runner}
247
+ # @!visibility protected
248
+ def runner(klass = nil)
249
+ @config.for(:runner, klass) { Teckel::Operation::Runner }
250
+ end
251
+
252
+ # @overload result()
253
+ # Get the configured result object class wrapping {error} or {output}.
254
+ # The {ValueResult} default will act as a pass-through and does. Any error
255
+ # or output will just returned as-is.
256
+ # @return [Class] The +result+ class, or {ValueResult} as default
257
+ #
258
+ # @overload result(klass)
259
+ # Set the result object class wrapping {error} or {output}.
260
+ # @param klass [Class] The +result+ class
261
+ # @return [Class] The +result+ class configured
262
+ def result(klass = nil)
263
+ @config.for(:result, klass) { const_defined?(:Result, false) ? self::Result : ValueResult }
264
+ end
265
+
266
+ # @overload result_constructor()
267
+ # The callable constructor to build an instance of the +result+ class.
268
+ # Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
269
+ # @return [Proc] A callable that will return an instance of +result+ class.
270
+ #
271
+ # @overload result_constructor(sym_or_proc)
272
+ # Define how to build the +result+.
273
+ # @param sym_or_proc [Symbol, #call]
274
+ # - Either a +Symbol+ representing the _public_ method to call on the +result+ class.
275
+ # - Or anything that response to +#call+ (like a +Proc+).
276
+ # @return [#call] The callable constructor
277
+ #
278
+ # @example
279
+ # class MyOperation
280
+ # include Teckel::Operation
281
+ #
282
+ # class Result
283
+ # include Teckel::Result
284
+ # def initialize(value, success, opts = {}); end
285
+ # end
286
+ #
287
+ # # If you need more control over how to build a new +Result+ instance
288
+ # result_constructor ->(value, success) { result.new(value, success, {foo: :bar}) }
289
+ # end
290
+ def result_constructor(sym_or_proc = nil)
291
+ get_set_constructor(:result_constructor, result, sym_or_proc) ||
292
+ raise(MissingConfigError, "Missing result_constructor config for #{self}")
293
+ end
294
+
295
+ # @!group Shortcuts
296
+
297
+ # Shortcut to use {Teckel::Operation::Result} as a result object,
298
+ # wrapping any {error} or {output}.
299
+ #
300
+ # @!visibility protected
301
+ # @note Don't use in conjunction with {result} or {result_constructor}
302
+ # @return [nil]
303
+ def result!
304
+ @config.for(:result, Teckel::Operation::Result)
305
+ @config.for(:result_constructor, Teckel::Operation::Result.method(:new))
306
+ nil
307
+ end
308
+
309
+ # @!visibility private
310
+ REQUIRED_CONFIGS = %i[
311
+ input input_constructor
312
+ output output_constructor
313
+ error error_constructor
314
+ settings settings_constructor
315
+ result result_constructor
316
+ runner
317
+ ].freeze
318
+
319
+ # @!visibility private
320
+ # @return [void]
321
+ def define!
322
+ REQUIRED_CONFIGS.each { |e| public_send(e) }
323
+ nil
324
+ end
325
+
326
+ # Disallow any further changes to this Operation.
327
+ # Make sure all configurations are set.
328
+ #
329
+ # @raise [MissingConfigError]
330
+ # @return [self] Frozen self
331
+ # @!visibility public
332
+ def finalize!
333
+ define!
334
+ @config.freeze
335
+ self
336
+ end
337
+
338
+ # Produces a shallow copy of this operation and all it's configuration.
339
+ #
340
+ # @return [self]
341
+ # @!visibility public
342
+ def dup
343
+ super.tap do |copy|
344
+ copy.instance_variable_set(:@config, @config.dup)
345
+ end
346
+ end
347
+
348
+ # Produces a clone of this operation and all it's configuration
349
+ #
350
+ # @return [self]
351
+ # @!visibility public
352
+ def clone
353
+ if frozen?
354
+ super
355
+ else
356
+ super.tap do |copy|
357
+ copy.instance_variable_set(:@config, @config.dup)
358
+ end
359
+ end
360
+ end
361
+
362
+ # @!visibility private
363
+ def inherited(subclass)
364
+ subclass.instance_variable_set(:@config, @config.dup)
365
+ end
366
+
367
+ # @!visibility private
368
+ def self.extended(base)
369
+ base.instance_exec do
370
+ @config = Teckel::Config.new
371
+ attr_accessor :settings
372
+ end
373
+ end
374
+
375
+ private
376
+
377
+ def get_set_constructor(name, on, sym_or_proc)
378
+ constructor = build_constructor(on, sym_or_proc) unless sym_or_proc.nil?
379
+
380
+ @config.for(name, constructor) {
381
+ build_constructor(on, Teckel::DEFAULT_CONSTRUCTOR)
382
+ }
383
+ end
384
+
385
+ def build_constructor(on, sym_or_proc)
386
+ if sym_or_proc.is_a?(Symbol) && on.respond_to?(sym_or_proc)
387
+ on.public_method(sym_or_proc)
388
+ elsif sym_or_proc.respond_to?(:call)
389
+ sym_or_proc
390
+ end
391
+ end
392
+ end
393
+ end
394
+ end
@@ -19,7 +19,7 @@ module Teckel
19
19
  err = catch(:failure) do
20
20
  simple_return = UNDEFINED
21
21
  out = catch(:success) do
22
- simple_return = call!(build_input(input))
22
+ simple_return = run(build_input(input))
23
23
  end
24
24
  return simple_return == UNDEFINED ? build_output(*out) : build_output(simple_return)
25
25
  end
@@ -34,7 +34,7 @@ module Teckel
34
34
 
35
35
  private
36
36
 
37
- def call!(input)
37
+ def run(input)
38
38
  op = @operation.new
39
39
  op.settings = settings if settings != UNDEFINED
40
40
  op.call(input)
@@ -47,10 +47,10 @@ module Teckel
47
47
  end
48
48
 
49
49
  def deconstruct_keys(keys)
50
- {}.tap do |e|
51
- e[:success] = successful? if keys.include?(:success)
52
- e[:value] = value if keys.include?(:value)
53
- end
50
+ e = {}
51
+ e[:success] = successful? if keys.include?(:success)
52
+ e[:value] = value if keys.include?(:value)
53
+ e
54
54
  end
55
55
  end
56
56
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Teckel
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Teckel::Chain do
4
+ module TeckelChainDefaultSettingsTest
5
+ class MyOperation
6
+ include Teckel::Operation
7
+ result!
8
+
9
+ settings Struct.new(:say, :other)
10
+ settings_constructor ->(data) { settings.new(*data.values_at(*settings.members)) } # ruby 2.4 way for `keyword_init: true`
11
+
12
+ input none
13
+ output Hash
14
+ error none
15
+
16
+ def call(_)
17
+ settings.to_h
18
+ end
19
+ end
20
+
21
+ class Chain
22
+ include Teckel::Chain
23
+
24
+ default_settings!(a: { say: "Chain Default" })
25
+
26
+ step :a, MyOperation
27
+ end
28
+ end
29
+
30
+ specify "call chain without settings, uses default settings" do
31
+ result = TeckelChainDefaultSettingsTest::Chain.call
32
+ expect(result.success).to eq(say: "Chain Default", other: nil)
33
+ end
34
+
35
+ specify "call chain with explicit settings, overwrites defaults" do
36
+ result = TeckelChainDefaultSettingsTest::Chain.with(a: { other: "What" }).call
37
+ expect(result.success).to eq(say: nil, other: "What")
38
+ end
39
+ end