teckel 0.1.0 → 0.6.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +114 -0
  3. data/LICENSE_LOGO +4 -0
  4. data/README.md +22 -14
  5. data/lib/teckel.rb +11 -2
  6. data/lib/teckel/chain.rb +47 -152
  7. data/lib/teckel/chain/config.rb +246 -0
  8. data/lib/teckel/chain/result.rb +38 -0
  9. data/lib/teckel/chain/runner.rb +62 -0
  10. data/lib/teckel/chain/step.rb +18 -0
  11. data/lib/teckel/config.rb +41 -52
  12. data/lib/teckel/contracts.rb +19 -0
  13. data/lib/teckel/operation.rb +108 -253
  14. data/lib/teckel/operation/config.rb +396 -0
  15. data/lib/teckel/operation/result.rb +92 -0
  16. data/lib/teckel/operation/runner.rb +75 -0
  17. data/lib/teckel/result.rb +52 -53
  18. data/lib/teckel/version.rb +1 -1
  19. data/spec/chain/default_settings_spec.rb +39 -0
  20. data/spec/chain/inheritance_spec.rb +116 -0
  21. data/spec/chain/none_input_spec.rb +36 -0
  22. data/spec/chain/results_spec.rb +53 -0
  23. data/spec/chain_around_hook_spec.rb +100 -0
  24. data/spec/chain_spec.rb +180 -0
  25. data/spec/config_spec.rb +26 -0
  26. data/spec/doctest_helper.rb +7 -0
  27. data/spec/operation/contract_trace_spec.rb +116 -0
  28. data/spec/operation/default_settings_spec.rb +94 -0
  29. data/spec/operation/inheritance_spec.rb +94 -0
  30. data/spec/operation/result_spec.rb +34 -0
  31. data/spec/operation/results_spec.rb +117 -0
  32. data/spec/operation_spec.rb +483 -0
  33. data/spec/rb27/pattern_matching_spec.rb +193 -0
  34. data/spec/result_spec.rb +22 -0
  35. data/spec/spec_helper.rb +25 -0
  36. data/spec/support/dry_base.rb +8 -0
  37. data/spec/support/fake_db.rb +12 -0
  38. data/spec/support/fake_models.rb +20 -0
  39. data/spec/teckel_spec.rb +7 -0
  40. metadata +64 -46
  41. data/.github/workflows/ci.yml +0 -67
  42. data/.github/workflows/pages.yml +0 -50
  43. data/.gitignore +0 -13
  44. data/.rspec +0 -3
  45. data/.rubocop.yml +0 -12
  46. data/.ruby-version +0 -1
  47. data/DEVELOPMENT.md +0 -28
  48. data/Gemfile +0 -8
  49. data/Gemfile.lock +0 -71
  50. data/Rakefile +0 -30
  51. data/bin/console +0 -15
  52. data/bin/rake +0 -29
  53. data/bin/rspec +0 -29
  54. data/bin/rubocop +0 -18
  55. data/bin/setup +0 -8
  56. data/lib/teckel/operation/results.rb +0 -71
  57. data/teckel.gemspec +0 -33
@@ -0,0 +1,396 @@
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
+ super subclass
366
+ end
367
+
368
+ # @!visibility private
369
+ def self.extended(base)
370
+ base.instance_exec do
371
+ @config = Teckel::Config.new
372
+ attr_accessor :runner
373
+ attr_accessor :settings
374
+ end
375
+ end
376
+
377
+ private
378
+
379
+ def get_set_constructor(name, on, sym_or_proc)
380
+ constructor = build_constructor(on, sym_or_proc) unless sym_or_proc.nil?
381
+
382
+ @config.for(name, constructor) {
383
+ build_constructor(on, Teckel::DEFAULT_CONSTRUCTOR)
384
+ }
385
+ end
386
+
387
+ def build_constructor(on, sym_or_proc)
388
+ if sym_or_proc.is_a?(Symbol) && on.respond_to?(sym_or_proc)
389
+ on.public_method(sym_or_proc)
390
+ elsif sym_or_proc.respond_to?(:call)
391
+ sym_or_proc
392
+ end
393
+ end
394
+ end
395
+ end
396
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teckel
4
+ module Operation
5
+ # The optional, default result object for {Teckel::Operation}s.
6
+ # Wraps +output+ and +error+ into a {Teckel::Operation::Result}.
7
+ #
8
+ # @example
9
+ # class CreateUser
10
+ # include Teckel::Operation
11
+ #
12
+ # result! # Shortcut to use this Result object
13
+ #
14
+ # input Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer)
15
+ # output Types.Instance(User)
16
+ # error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
17
+ #
18
+ # def call(input)
19
+ # user = User.new(name: input[:name], age: input[:age])
20
+ # if user.save
21
+ # success!(user) # exits early with success, prevents any further execution
22
+ # else
23
+ # fail!(message: "Could not save User", errors: user.errors)
24
+ # end
25
+ # end
26
+ # end
27
+ #
28
+ # # A success call:
29
+ # CreateUser.call(name: "Bob", age: 23).is_a?(Teckel::Operation::Result) #=> true
30
+ # CreateUser.call(name: "Bob", age: 23).success.is_a?(User) #=> true
31
+ #
32
+ # # A failure call:
33
+ # CreateUser.call(name: "Bob", age: 10).is_a?(Teckel::Operation::Result) #=> true
34
+ # CreateUser.call(name: "Bob", age: 10).failure.is_a?(Hash) #=> true
35
+ #
36
+ # @!visibility public
37
+ class Result
38
+ include Teckel::Result
39
+
40
+ # @param value [Object] The result value
41
+ # @param success [Boolean] whether this is a successful result
42
+ def initialize(value, success)
43
+ @value = value
44
+ @success = (!!success).freeze
45
+ end
46
+
47
+ # Whether this is a success result
48
+ # @return [Boolean]
49
+ def successful?
50
+ @success
51
+ end
52
+
53
+ # @!attribute [r] value
54
+ # @return [Mixed] the value/payload
55
+ attr_reader :value
56
+
57
+ # Get the error/failure value
58
+ # @yield [Mixed] If a block is given and this is not a failure result, the value is yielded to the block
59
+ # @param default [Mixed] return this default value if it's not a failure result
60
+ # @return [Mixed] the value/payload
61
+ def failure(default = nil, &block)
62
+ return @value unless @success
63
+ return yield(@value) if block
64
+
65
+ default
66
+ end
67
+
68
+ # Get the success value
69
+ # @yield [Mixed] If a block is given and this is not a success result, the value is yielded to the block
70
+ # @param default [Mixed] return this default value if it's not a success result
71
+ # @return [Mixed] the value/payload
72
+ def success(default = nil, &block)
73
+ return @value if @success
74
+ return yield(@value) if block
75
+
76
+ default
77
+ end
78
+ end
79
+
80
+ # The default "no-op" Result handler. Just returns the value, ignoring the
81
+ # success state.
82
+ module ValueResult
83
+ class << self
84
+ def [](value, *_)
85
+ value
86
+ end
87
+
88
+ alias :new :[]
89
+ end
90
+ end
91
+ end
92
+ end