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,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
  #
@@ -9,47 +13,15 @@ module Teckel
9
13
  # the constants +Input+, +Output+ and +Error+, the second way is to use the
10
14
  # +input+. +output+ and +error+ methods to point them to anonymous classes.
11
15
  #
12
- # If you like "traditional" result objects (to ask +successful?+ or +failure?+ on)
13
- # see +Teckel::Operation::Results+
14
- #
15
- # @see Teckel::Operation::Results
16
- #
17
- # @example class definitions via constants
18
- # class CreateUserViaConstants
19
- # include Teckel::Operation
20
- #
21
- # class Input
22
- # def initialize(name:, age:)
23
- # @name, @age = name, age
24
- # end
25
- # attr_reader :name, :age
26
- # end
27
- #
28
- # Output = ::User
29
- #
30
- # class Error
31
- # def initialize(message, errors)
32
- # @message, @errors = message, errors
33
- # end
34
- # attr_reader :message, :errors
35
- # end
36
- #
37
- # input_constructor :new
38
- # error_constructor :new
16
+ # If you like "traditional" result objects to ask +successful?+ or +failure?+ on,
17
+ # use {Teckel::Operation::Config#result! result!} and get {Teckel::Operation::Result}
39
18
  #
40
- # # @param [CreateUser::Input]
41
- # # @return [User | CreateUser::Error]
42
- # def call(input)
43
- # user = ::User.new(name: input.name, age: input.age)
44
- # if user.safe
45
- # user
46
- # else
47
- # fail!(message: "Could not safe User", errors: user.errors)
48
- # end
49
- # end
50
- # end
19
+ # By default, +input+. +output+ and +error+ classes are build using +:[]+
20
+ # (eg: +Input[some: :param]+).
51
21
  #
52
- # 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.
53
25
  #
54
26
  # @example class definitions via methods
55
27
  # class CreateUserViaMethods
@@ -60,14 +32,13 @@ module Teckel
60
32
  # error Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))
61
33
  #
62
34
  # # @param [Hash<name: String, age: Integer>]
63
- # # @return [User | Hash<message: String, errors: [Hash]>]
35
+ # # @return [User,Hash<message: String, errors: [Hash]>]
64
36
  # def call(input)
65
37
  # user = User.new(name: input[:name], age: input[:age])
66
- # if user.safe
67
- # # exits early with success, prevents any further execution
68
- # success!(user)
38
+ # if user.save
39
+ # success!(user) # exits early with success, prevents any further execution
69
40
  # else
70
- # fail!(message: "Could not safe User", errors: user.errors)
41
+ # fail!(message: "Could not save User", errors: user.errors)
71
42
  # end
72
43
  # end
73
44
  # end
@@ -76,7 +47,7 @@ module Teckel
76
47
  # CreateUserViaMethods.call(name: "Bob", age: 23).is_a?(User) #=> true
77
48
  #
78
49
  # # A failure call:
79
- # CreateUserViaMethods.call(name: "Bob", age: 10).eql?(message: "Could not safe User", errors: [{age: "underage"}]) #=> true
50
+ # CreateUserViaMethods.call(name: "Bob", age: 10).eql?(message: "Could not save User", errors: [{age: "underage"}]) #=> true
80
51
  #
81
52
  # # Build your Input, Output and Error classes in a way that let you know:
82
53
  # begin; CreateUserViaMethods.call(unwanted: "input"); rescue => e; e end.is_a?(::Dry::Types::MissingKeyError) #=> true
@@ -84,257 +55,141 @@ module Teckel
84
55
  # # Feed an instance of the input class directly to call:
85
56
  # CreateUserViaMethods.call(CreateUserViaMethods.input[name: "Bob", age: 23]).is_a?(User) #=> true
86
57
  #
87
- # @api public
58
+ # @!visibility public
88
59
  module Operation
89
60
  module ClassMethods
90
- # @!attribute [r] input()
91
- # Get the configured class wrapping the input data structure.
92
- # @return [Class] The +input+ class
93
-
94
- # @!method input(klass)
95
- # Set the class wrapping the input data structure.
96
- # @param klass [Class] The +input+ class
97
- # @return [Class] The +input+ class
98
- def input(klass = nil)
99
- return @input_class if @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
100
70
 
101
- @input_class = @config.input(klass)
102
- @input_class ||= self::Input if const_defined?(:Input)
103
- @input_class
71
+ if default_settings
72
+ runner.new(self, default_settings.call)
73
+ else
74
+ runner.new(self)
75
+ end.call(input)
104
76
  end
105
77
 
106
- # @!attribute [r] input_constructor()
107
- # The callable constructor to build an instance of the +input+ class.
108
- # @return [Class] The Input class
109
-
110
- # @!method input_constructor(sym_or_proc)
111
- # Define how to build the +input+.
112
- # @param sym_or_proc [Symbol|#call]
113
- # - Either a +Symbol+ representing the _public_ method to call on the +input+ class.
114
- # - Or a callable (like a +Proc+).
115
- # @return [#call] The callable constructor
116
- #
117
- # @example simple symbol to method constructor
118
- # class MyOperation
119
- # include Teckel::Operation
78
+ # Provide {InstanceMethods#settings() settings} to the running operation.
120
79
  #
121
- # class Input
122
- # # ...
123
- # end
80
+ # This method is intended to be called on the operation class outside of
81
+ # it's definition, prior to running {#call}.
124
82
  #
125
- # # If you need more control over how to build a new +Input+ instance
126
- # # MyOperation.call(name: "Bob", age: 23) # -> Input.new(name: "Bob", age: 23)
127
- # input_constructor :new
128
- # 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
129
87
  #
130
- # MyOperation.input_constructor.is_a?(Method) #=> true
88
+ # @example Inject settings for an operation call
89
+ # LOG = []
131
90
  #
132
- # @example Custom Proc constructor
133
91
  # class MyOperation
134
- # include Teckel::Operation
92
+ # include ::Teckel::Operation
135
93
  #
136
- # class Input
137
- # # ...
138
- # end
94
+ # settings Struct.new(:log)
95
+ #
96
+ # input none
97
+ # output none
98
+ # error none
139
99
  #
140
- # # If you need more control over how to build a new +Input+ instance
141
- # # MyOperation.call("foo", opt: "bar") # -> Input.new(name: "foo", opt: "bar")
142
- # input_constructor ->(name, options) { Input.new(name: name, **options) }
100
+ # def call(_input)
101
+ # settings.log << "called" if settings&.log
102
+ # nil
103
+ # end
143
104
  # end
144
105
  #
145
- # MyOperation.input_constructor.is_a?(Proc) #=> true
146
- def input_constructor(sym_or_proc = nil)
147
- return @input_constructor if @input_constructor
148
-
149
- constructor = @config.input_constructor(sym_or_proc)
150
- @input_constructor =
151
- if constructor.is_a?(Symbol) && input.respond_to?(constructor)
152
- input.public_method(constructor)
153
- elsif sym_or_proc.respond_to?(:call)
154
- sym_or_proc
155
- end
156
- end
157
-
158
- # @!attribute [r] output()
159
- # Get the configured class wrapping the output data structure.
160
- # @return [Class] The +output+ class
161
-
162
- # @!method output(klass)
163
- # Set the class wrapping the output data structure.
164
- # @param klass [Class] The +output+ class
165
- # @return [Class] The +output+ class
166
- def output(klass = nil)
167
- return @output_class if @output_class
168
-
169
- @output_class = @config.output(klass)
170
- @output_class ||= self::Output if const_defined?(:Output)
171
- @output_class
106
+ # MyOperation.with(LOG).call
107
+ # LOG #=> ["called"]
108
+ #
109
+ # LOG.clear
110
+ #
111
+ # MyOperation.with(false).call
112
+ # MyOperation.call
113
+ # LOG #=> []
114
+ def with(input)
115
+ runner.new(self, settings_constructor.call(input))
172
116
  end
117
+ alias :set :with
173
118
 
174
- # @!attribute [r] output_constructor()
175
- # The callable constructor to build an instance of the +output+ class.
176
- # @return [Class] The Output class
177
-
178
- # @!method output_constructor(sym_or_proc)
179
- # Define how to build the +output+.
180
- # @param sym_or_proc [Symbol|#call]
181
- # - Either a +Symbol+ representing the _public_ method to call on the +output+ class.
182
- # - Or a callable (like a +Proc+).
183
- # @return [#call] The callable constructor
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.
184
123
  #
185
- # @example
124
+ # @return [Object] The {Teckel::Contracts::None} class.
125
+ #
126
+ # @example Enforcing nil input, output or error
186
127
  # class MyOperation
187
128
  # include Teckel::Operation
188
129
  #
189
- # class Output
190
- # # ....
191
- # end
130
+ # input none
192
131
  #
193
- # # MyOperation.call("foo", "bar") # -> Output.new("foo", "bar")
194
- # output_constructor :new
132
+ # # same as
133
+ # output Teckel::Contracts::None
195
134
  #
196
- # # If you need more control over how to build a new +Output+ instance
197
- # # MyOperation.call("foo", opt: "bar") # -> Output.new(name: "foo", opt: "bar")
198
- # output_constructor ->(name, options) { Output.new(name: name, **options) }
199
- # end
200
- def output_constructor(sym_or_proc = nil)
201
- return @output_constructor if @output_constructor
202
-
203
- constructor = @config.output_constructor(sym_or_proc)
204
- @output_constructor =
205
- if constructor.is_a?(Symbol) && output.respond_to?(constructor)
206
- output.public_method(constructor)
207
- elsif sym_or_proc.respond_to?(:call)
208
- sym_or_proc
209
- end
210
- end
211
-
212
- # @!attribute [r] error()
213
- # Get the configured class wrapping the error data structure.
214
- # @return [Class] The +error+ class
215
-
216
- # @!method error(klass)
217
- # Set the class wrapping the error data structure.
218
- # @param klass [Class] The +error+ class
219
- # @return [Class] The +error+ class
220
- def error(klass = nil)
221
- return @error_class if @error_class
222
-
223
- @error_class = @config.error(klass)
224
- @error_class ||= self::Error if const_defined?(:Error)
225
- @error_class
226
- end
227
-
228
- # @!attribute [r] error_constructor()
229
- # The callable constructor to build an instance of the +error+ class.
230
- # @return [Class] The Error class
231
-
232
- # @!method error_constructor(sym_or_proc)
233
- # Define how to build the +error+.
234
- # @param sym_or_proc [Symbol|#call]
235
- # - Either a +Symbol+ representing the _public_ method to call on the +error+ class.
236
- # - Or a callable (like a +Proc+).
237
- # @return [#call] The callable constructor
135
+ # error none
238
136
  #
239
- # @example
240
- # class MyOperation
241
- # include Teckel::Operation
137
+ # def call(_) # you still need to take than +nil+ input when using `input none`
138
+ # # when using `error none`:
139
+ # # `fail!` works, but `fail!("data")` raises an error
242
140
  #
243
- # class Error
244
- # # ....
141
+ # # when using `output none`:
142
+ # # `success!` works, but `success!("data")` raises an error
245
143
  # end
246
- #
247
- # # MyOperation.call("foo", "bar") # -> Error.new("foo", "bar")
248
- # error_constructor :new
249
- #
250
- # # If you need more control over how to build a new +Error+ instance
251
- # # MyOperation.call("foo", opt: "bar") # -> Error.new(name: "foo", opt: "bar")
252
- # error_constructor ->(name, options) { Error.new(name: name, **options) }
253
144
  # end
254
- def error_constructor(sym_or_proc = nil)
255
- return @error_constructor if @error_constructor
256
-
257
- constructor = @config.error_constructor(sym_or_proc)
258
- @error_constructor =
259
- if constructor.is_a?(Symbol) && error.respond_to?(constructor)
260
- error.public_method(constructor)
261
- elsif sym_or_proc.respond_to?(:call)
262
- sym_or_proc
263
- end
264
- end
265
-
266
- # Invoke the Operation
267
145
  #
268
- # @param input Any form of input your +input+ class can handle via the given +input_constructor+
269
- # @return Either An instance of your defined +error+ class or +output+ class
270
- def call(input)
271
- new.call!(input)
146
+ # MyOperation.call #=> nil
147
+ def none
148
+ Teckel::Contracts::None
272
149
  end
273
150
  end
274
151
 
275
152
  module InstanceMethods
276
- # @!visibility protected
277
- def call!(input)
278
- catch(:failure) do
279
- out = catch(:success) do
280
- simple_ret = call(build_input(input))
281
- build_output(simple_ret)
282
- end
283
- return out
284
- end
285
- end
286
-
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
168
+ # @!visibility public
169
+
170
+ # Delegates to the configured Runner.
171
+ # The default behavior is to halt any further execution with a output value.
172
+ #
173
+ # @see Teckel::Operation::Runner#success!
287
174
  # @!visibility protected
288
175
  def success!(*args)
289
- throw :success, build_output(*args)
176
+ runner.success!(*args)
290
177
  end
291
178
 
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!
292
183
  # @!visibility protected
293
184
  def fail!(*args)
294
- throw :failure, build_error(*args)
295
- end
296
-
297
- private
298
-
299
- def build_input(input)
300
- self.class.input_constructor.call(input)
301
- end
302
-
303
- def build_output(*args)
304
- if args.size == 1 && self.class.output === args.first # rubocop:disable Style/CaseEquality
305
- args.first
306
- else
307
- self.class.output_constructor.call(*args)
308
- end
309
- end
310
-
311
- def build_error(*args)
312
- if args.size == 1 && self.class.error === args.first # rubocop:disable Style/CaseEquality
313
- args.first
314
- else
315
- self.class.error_constructor.call(*args)
316
- end
185
+ runner.fail!(*args)
317
186
  end
318
187
  end
319
188
 
320
189
  def self.included(receiver)
190
+ receiver.extend Config
321
191
  receiver.extend ClassMethods
322
192
  receiver.send :include, InstanceMethods
323
-
324
- receiver.class_eval do
325
- @config = Config.new
326
-
327
- @input_class = nil
328
- @input_constructor = nil
329
-
330
- @output_class = nil
331
- @output_constructor = nil
332
-
333
- @error_class = nil
334
- @error_constructor = nil
335
-
336
- protected :success!, :fail!
337
- end
338
193
  end
339
194
  end
340
195
  end