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.
@@ -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