teckel 0.2.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +111 -0
  3. data/LICENSE_LOGO +4 -0
  4. data/README.md +4 -4
  5. data/lib/teckel.rb +9 -4
  6. data/lib/teckel/chain.rb +31 -341
  7. data/lib/teckel/chain/config.rb +275 -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 +25 -28
  12. data/lib/teckel/contracts.rb +19 -0
  13. data/lib/teckel/operation.rb +84 -302
  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 +74 -0
  17. data/lib/teckel/result.rb +52 -53
  18. data/lib/teckel/version.rb +1 -1
  19. data/spec/chain/around_hook_spec.rb +100 -0
  20. data/spec/chain/default_settings_spec.rb +39 -0
  21. data/spec/chain/inheritance_spec.rb +116 -0
  22. data/spec/chain/none_input_spec.rb +36 -0
  23. data/spec/chain/results_spec.rb +53 -0
  24. data/spec/chain_spec.rb +180 -0
  25. data/spec/config_spec.rb +26 -0
  26. data/spec/doctest_helper.rb +8 -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/fail_on_input_spec.rb +103 -0
  30. data/spec/operation/inheritance_spec.rb +94 -0
  31. data/spec/operation/result_spec.rb +55 -0
  32. data/spec/operation/results_spec.rb +117 -0
  33. data/spec/operation_spec.rb +483 -0
  34. data/spec/rb27/pattern_matching_spec.rb +193 -0
  35. data/spec/result_spec.rb +22 -0
  36. data/spec/spec_helper.rb +28 -0
  37. data/spec/support/dry_base.rb +8 -0
  38. data/spec/support/fake_db.rb +12 -0
  39. data/spec/support/fake_models.rb +20 -0
  40. data/spec/teckel_spec.rb +7 -0
  41. metadata +68 -28
  42. data/.codeclimate.yml +0 -3
  43. data/.github/workflows/ci.yml +0 -92
  44. data/.github/workflows/pages.yml +0 -50
  45. data/.gitignore +0 -15
  46. data/.rspec +0 -3
  47. data/.rubocop.yml +0 -12
  48. data/.ruby-version +0 -1
  49. data/DEVELOPMENT.md +0 -32
  50. data/Gemfile +0 -16
  51. data/Rakefile +0 -35
  52. data/bin/console +0 -15
  53. data/bin/rake +0 -29
  54. data/bin/rspec +0 -29
  55. data/bin/rubocop +0 -18
  56. data/bin/setup +0 -8
  57. data/lib/teckel/none.rb +0 -18
  58. data/lib/teckel/operation/results.rb +0 -72
  59. data/teckel.gemspec +0 -32
@@ -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