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,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