teckel 0.2.0 → 0.7.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 (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,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teckel
4
+ module Contracts
5
+ # Simple contract for enforcing data to be not set or +nil+
6
+ module 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
19
+ end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "operation/config"
4
+ require_relative "operation/result"
5
+ require_relative "operation/runner"
6
+
3
7
  module Teckel
4
8
  # The main operation Mixin
5
9
  #
@@ -10,50 +14,14 @@ module Teckel
10
14
  # +input+. +output+ and +error+ methods to point them to anonymous classes.
11
15
  #
12
16
  # If you like "traditional" result objects to ask +successful?+ or +failure?+ on,
13
- # see {Teckel::Operation::Results Teckel::Operation::Results}
17
+ # use {Teckel::Operation::Config#result! result!} and get {Teckel::Operation::Result}
14
18
  #
15
19
  # By default, +input+. +output+ and +error+ classes are build using +:[]+
16
20
  # (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.
20
- #
21
- # @example class definitions via constants
22
- # class CreateUserViaConstants
23
- # include Teckel::Operation
24
- #
25
- # class Input
26
- # def initialize(name:, age:)
27
- # @name, @age = name, age
28
- # end
29
- # attr_reader :name, :age
30
- # end
31
- #
32
- # Output = ::User
33
- #
34
- # class Error
35
- # def initialize(message, errors)
36
- # @message, @errors = message, errors
37
- # end
38
- # attr_reader :message, :errors
39
- # end
40
- #
41
- # input_constructor :new
42
- # error_constructor :new
43
- #
44
- # # @param [CreateUser::Input]
45
- # # @return [User,CreateUser::Error]
46
- # def call(input)
47
- # user = ::User.new(name: input.name, age: input.age)
48
- # if user.save
49
- # user
50
- # else
51
- # fail!(message: "Could not save User", errors: user.errors)
52
- # end
53
- # end
54
- # end
55
21
  #
56
- # CreateUserViaConstants.call(name: "Bob", age: 23).is_a?(User) #=> true
22
+ # Use {Teckel::Operation::Config#input_constructor input_constructor},
23
+ # {Teckel::Operation::Config#output_constructor output_constructor} and
24
+ # {Teckel::Operation::Config#error_constructor error_constructor} to change them.
57
25
  #
58
26
  # @example class definitions via methods
59
27
  # class CreateUserViaMethods
@@ -89,201 +57,72 @@ module Teckel
89
57
  #
90
58
  # @!visibility public
91
59
  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
-
138
60
  module ClassMethods
139
- # @!attribute [r] input()
140
- # Get the configured class wrapping the input data structure.
141
- # @return [Class] The +input+ class
61
+ # Invoke the Operation
62
+ #
63
+ # @param input Any form of input your {Teckel::Operation::Config#input input} class can handle via the given
64
+ # {Teckel::Operation::Config#input_constructor input_constructor}
65
+ # @return Either An instance of your defined {Teckel::Operation::Config#error error} class or
66
+ # {Teckel::Operation::Config#output output} class
67
+ # @!visibility public
68
+ def call(input = nil)
69
+ default_settings = self.default_settings
142
70
 
143
- # @!method input(klass)
144
- # Set the class wrapping the input data structure.
145
- # @param klass [Class] The +input+ class
146
- # @return [Class] The +input+ class
147
- def input(klass = nil)
148
- @config.for(:input, klass) { self::Input if const_defined?(:Input) } ||
149
- raise(Teckel::MissingConfigError, "Missing input config for #{self}")
71
+ if default_settings
72
+ runner.new(self, default_settings.call)
73
+ else
74
+ runner.new(self)
75
+ end.call(input)
150
76
  end
151
77
 
152
- # @!attribute [r] input_constructor()
153
- # The callable constructor to build an instance of the +input+ class.
154
- # @return [Class] The Input class
155
-
156
- # @!method input_constructor(sym_or_proc)
157
- # Define how to build the +input+.
158
- # @param sym_or_proc [Symbol, #call]
159
- # - Either a +Symbol+ representing the _public_ method to call on the +input+ class.
160
- # - Or anything that response to +#call+ (like a +Proc+).
161
- # @return [#call] The callable constructor
162
- #
163
- # @example simple symbol to method constructor
164
- # class MyOperation
165
- # include Teckel::Operation
78
+ # Provide {InstanceMethods#settings() settings} to the running operation.
166
79
  #
167
- # class Input
168
- # def initialize(name:, age:); end
169
- # end
80
+ # This method is intended to be called on the operation class outside of
81
+ # it's definition, prior to running {#call}.
170
82
  #
171
- # # If you need more control over how to build a new +Input+ instance
172
- # # MyOperation.call(name: "Bob", age: 23) # -> Input.new(name: "Bob", age: 23)
173
- # input_constructor :new
174
- # end
83
+ # @param input Any form of input your {Teckel::Operation::Config#settings settings} class can handle via the given
84
+ # {Teckel::Operation::Config#settings_constructor settings_constructor}
85
+ # @return [Class] The configured {Teckel::Operation::Config#runner runner}
86
+ # @!visibility public
175
87
  #
176
- # MyOperation.input_constructor.is_a?(Method) #=> true
88
+ # @example Inject settings for an operation call
89
+ # LOG = []
177
90
  #
178
- # @example Custom Proc constructor
179
91
  # class MyOperation
180
- # include Teckel::Operation
181
- #
182
- # class Input
183
- # def initialize(*args, **opts); end
184
- # end
92
+ # include ::Teckel::Operation
185
93
  #
186
- # # If you need more control over how to build a new +Input+ instance
187
- # # MyOperation.call("foo", opt: "bar") # -> Input.new(name: "foo", opt: "bar")
188
- # input_constructor ->(name, options) { Input.new(name: name, **options) }
189
- # end
94
+ # settings Struct.new(:log)
190
95
  #
191
- # MyOperation.input_constructor.is_a?(Proc) #=> true
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}")
195
- end
196
-
197
- # @!attribute [r] output()
198
- # Get the configured class wrapping the output data structure.
199
- # @return [Class] The +output+ class
200
-
201
- # @!method output(klass)
202
- # Set the class wrapping the output data structure.
203
- # @param klass [Class] The +output+ class
204
- # @return [Class] The +output+ class
205
- def output(klass = nil)
206
- @config.for(:output, klass) { self::Output if const_defined?(:Output) } ||
207
- raise(Teckel::MissingConfigError, "Missing output config for #{self}")
208
- end
209
-
210
- # @!attribute [r] output_constructor()
211
- # The callable constructor to build an instance of the +output+ class.
212
- # @return [Class] The Output class
213
-
214
- # @!method output_constructor(sym_or_proc)
215
- # Define how to build the +output+.
216
- # @param sym_or_proc [Symbol, #call]
217
- # - Either a +Symbol+ representing the _public_ method to call on the +output+ class.
218
- # - Or anything that response to +#call+ (like a +Proc+).
219
- # @return [#call] The callable constructor
220
- #
221
- # @example
222
- # class MyOperation
223
- # include Teckel::Operation
96
+ # input none
97
+ # output none
98
+ # error none
224
99
  #
225
- # class Output
226
- # def initialize(*args, **opts); end
100
+ # def call(_input)
101
+ # settings.log << "called" if settings&.log
102
+ # nil
227
103
  # end
228
- #
229
- # # MyOperation.call("foo", "bar") # -> Output.new("foo", "bar")
230
- # output_constructor :new
231
- #
232
- # # If you need more control over how to build a new +Output+ instance
233
- # # MyOperation.call("foo", opt: "bar") # -> Output.new(name: "foo", opt: "bar")
234
- # output_constructor ->(name, options) { Output.new(name: name, **options) }
235
104
  # 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}")
239
- end
240
-
241
- # @!attribute [r] error()
242
- # Get the configured class wrapping the error data structure.
243
- # @return [Class] The +error+ class
244
-
245
- # @!method error(klass)
246
- # Set the class wrapping the error data structure.
247
- # @param klass [Class] The +error+ class
248
- # @return [Class,nil] The +error+ class or +nil+ if it does not error
249
- def error(klass = nil)
250
- @config.for(:error, klass) { self::Error if const_defined?(:Error) } ||
251
- raise(Teckel::MissingConfigError, "Missing error config for #{self}")
252
- end
253
-
254
- # @!attribute [r] error_constructor()
255
- # The callable constructor to build an instance of the +error+ class.
256
- # @return [Class] The Error class
257
-
258
- # @!method error_constructor(sym_or_proc)
259
- # Define how to build the +error+.
260
- # @param sym_or_proc [Symbol, #call]
261
- # - Either a +Symbol+ representing the _public_ method to call on the +error+ class.
262
- # - Or anything that response to +#call+ (like a +Proc+).
263
- # @return [#call] The callable constructor
264
105
  #
265
- # @example
266
- # class MyOperation
267
- # include Teckel::Operation
268
- #
269
- # class Error
270
- # def initialize(*args, **opts); end
271
- # end
106
+ # MyOperation.with(LOG).call
107
+ # LOG #=> ["called"]
272
108
  #
273
- # # MyOperation.call("foo", "bar") # -> Error.new("foo", "bar")
274
- # error_constructor :new
109
+ # LOG.clear
275
110
  #
276
- # # If you need more control over how to build a new +Error+ instance
277
- # # MyOperation.call("foo", opt: "bar") # -> Error.new(name: "foo", opt: "bar")
278
- # error_constructor ->(name, options) { Error.new(name: name, **options) }
279
- # 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}")
111
+ # MyOperation.with(false).call
112
+ # MyOperation.call
113
+ # LOG #=> []
114
+ def with(input)
115
+ runner.new(self, settings_constructor.call(input))
283
116
  end
117
+ alias :set :with
284
118
 
285
- # Convenience method for setting {#input}, {#output} or {#error} to the {None} value.
286
- # @return [None]
119
+ # Convenience method for setting {Teckel::Operation::Config#input input},
120
+ # {Teckel::Operation::Config#output output} or
121
+ # {Teckel::Operation::Config#error error} to the
122
+ # {Teckel::Contracts::None} value.
123
+ #
124
+ # @return [Object] The {Teckel::Contracts::None} class.
125
+ #
287
126
  # @example Enforcing nil input, output or error
288
127
  # class MyOperation
289
128
  # include Teckel::Operation
@@ -291,7 +130,7 @@ module Teckel
291
130
  # input none
292
131
  #
293
132
  # # same as
294
- # output Teckel::None
133
+ # output Teckel::Contracts::None
295
134
  #
296
135
  # error none
297
136
  #
@@ -301,113 +140,56 @@ module Teckel
301
140
  #
302
141
  # # when using `output none`:
303
142
  # # `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
143
  # end
308
144
  # end
309
145
  #
310
146
  # MyOperation.call #=> nil
311
147
  def none
312
- None
313
- end
314
-
315
- # @!attribute [r] runner()
316
- # @return [Class] The Runner class
317
- # @!visibility protected
318
-
319
- # Overwrite the default runner
320
- # @param klass [Class] A class like the {Runner}
321
- # @!visibility protected
322
- def runner(klass = nil)
323
- @config.for(:runner, klass) { Runner }
148
+ Teckel::Contracts::None
324
149
  end
150
+ end
325
151
 
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
152
+ module InstanceMethods
153
+ # @!method call(input)
154
+ # @abstract
155
+ # @see Operation
156
+ # @see ClassMethods#call
157
+ #
158
+ # The entry point for your operation. It needs to always accept an input value, even when
159
+ # using +input none+.
160
+ # If your Operation expects to generate success or failure outputs, you need to use either
161
+ # {.success!} or {.fail!} respectively. Simple return values will get ignored by default. See
162
+ # {Teckel::Operation::Config#runner} and {Teckel::Operation::Runner} on how to overwrite.
163
+
164
+ # @!attribute [r] settings()
165
+ # @return [Class,nil] When executed with settings, an instance of the
166
+ # configured {.settings} class. Otherwise +nil+
167
+ # @see ClassMethods#settings
330
168
  # @!visibility public
331
- def call(input = nil)
332
- runner.new(self).call(input)
333
- end
334
-
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
343
169
 
344
- # Disallow any further changes to this Operation.
345
- # Make sure all configurations are set.
170
+ # Delegates to the configured Runner.
171
+ # The default behavior is to halt any further execution with a output value.
346
172
  #
347
- # @raise [MissingConfigError]
348
- # @return [self] Frozen self
349
- # @!visibility public
350
- def finalize!
351
- define!
352
- freeze
353
- @config.freeze
354
- self
355
- end
356
-
357
- # @!visibility public
358
- def dup
359
- super.tap do |copy|
360
- copy.instance_variable_set(:@config, @config.dup)
361
- end
362
- end
363
-
364
- # @!visibility public
365
- def clone
366
- if frozen?
367
- super
368
- else
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
382
- end
383
- end
384
- end
385
-
386
- module InstanceMethods
387
- # Halt any further execution with a +output+ value
173
+ # @see Teckel::Operation::Runner#success!
388
174
  # @!visibility protected
389
175
  def success!(*args)
390
- throw :success, args
176
+ runner.success!(*args)
391
177
  end
392
178
 
393
- # Halt any further execution with an +error+ value
179
+ # Delegates to the configured Runner.
180
+ # The default behavior is to halt any further execution with an error value.
181
+ #
182
+ # @see Teckel::Operation::Runner#fail!
394
183
  # @!visibility protected
395
184
  def fail!(*args)
396
- throw :failure, args
185
+ runner.fail!(*args)
397
186
  end
398
187
  end
399
188
 
400
189
  def self.included(receiver)
190
+ receiver.extend Config
401
191
  receiver.extend ClassMethods
402
192
  receiver.send :include, InstanceMethods
403
-
404
- receiver.class_eval do
405
- @config = Config.new
406
-
407
- @runner = Runner
408
-
409
- protected :success!, :fail!
410
- end
411
193
  end
412
194
  end
413
195
  end