teckel 0.1.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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