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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +114 -0
- data/LICENSE_LOGO +4 -0
- data/README.md +22 -14
- data/lib/teckel.rb +11 -2
- data/lib/teckel/chain.rb +47 -152
- data/lib/teckel/chain/config.rb +246 -0
- data/lib/teckel/chain/result.rb +38 -0
- data/lib/teckel/chain/runner.rb +62 -0
- data/lib/teckel/chain/step.rb +18 -0
- data/lib/teckel/config.rb +41 -52
- data/lib/teckel/contracts.rb +19 -0
- data/lib/teckel/operation.rb +108 -253
- data/lib/teckel/operation/config.rb +396 -0
- data/lib/teckel/operation/result.rb +92 -0
- data/lib/teckel/operation/runner.rb +75 -0
- data/lib/teckel/result.rb +52 -53
- data/lib/teckel/version.rb +1 -1
- data/spec/chain/default_settings_spec.rb +39 -0
- data/spec/chain/inheritance_spec.rb +116 -0
- data/spec/chain/none_input_spec.rb +36 -0
- data/spec/chain/results_spec.rb +53 -0
- data/spec/chain_around_hook_spec.rb +100 -0
- data/spec/chain_spec.rb +180 -0
- data/spec/config_spec.rb +26 -0
- data/spec/doctest_helper.rb +7 -0
- data/spec/operation/contract_trace_spec.rb +116 -0
- data/spec/operation/default_settings_spec.rb +94 -0
- data/spec/operation/inheritance_spec.rb +94 -0
- data/spec/operation/result_spec.rb +34 -0
- data/spec/operation/results_spec.rb +117 -0
- data/spec/operation_spec.rb +483 -0
- data/spec/rb27/pattern_matching_spec.rb +193 -0
- data/spec/result_spec.rb +22 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/dry_base.rb +8 -0
- data/spec/support/fake_db.rb +12 -0
- data/spec/support/fake_models.rb +20 -0
- data/spec/teckel_spec.rb +7 -0
- metadata +64 -46
- data/.github/workflows/ci.yml +0 -67
- data/.github/workflows/pages.yml +0 -50
- data/.gitignore +0 -13
- data/.rspec +0 -3
- data/.rubocop.yml +0 -12
- data/.ruby-version +0 -1
- data/DEVELOPMENT.md +0 -28
- data/Gemfile +0 -8
- data/Gemfile.lock +0 -71
- data/Rakefile +0 -30
- data/bin/console +0 -15
- data/bin/rake +0 -29
- data/bin/rspec +0 -29
- data/bin/rubocop +0 -18
- data/bin/setup +0 -8
- data/lib/teckel/operation/results.rb +0 -71
- 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
|
data/lib/teckel/operation.rb
CHANGED
@@ -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
|
13
|
-
#
|
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
|
-
#
|
41
|
-
#
|
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
|
-
#
|
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
|
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.
|
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
|
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
|
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
|
-
#
|
58
|
+
# @!visibility public
|
88
59
|
module Operation
|
89
60
|
module ClassMethods
|
90
|
-
#
|
91
|
-
#
|
92
|
-
# @
|
93
|
-
|
94
|
-
#
|
95
|
-
#
|
96
|
-
#
|
97
|
-
|
98
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
126
|
-
#
|
127
|
-
#
|
128
|
-
#
|
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
|
-
#
|
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
|
-
#
|
137
|
-
#
|
138
|
-
#
|
94
|
+
# settings Struct.new(:log)
|
95
|
+
#
|
96
|
+
# input none
|
97
|
+
# output none
|
98
|
+
# error none
|
139
99
|
#
|
140
|
-
#
|
141
|
-
#
|
142
|
-
#
|
100
|
+
# def call(_input)
|
101
|
+
# settings.log << "called" if settings&.log
|
102
|
+
# nil
|
103
|
+
# end
|
143
104
|
# end
|
144
105
|
#
|
145
|
-
# MyOperation.
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
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
|
-
#
|
175
|
-
#
|
176
|
-
#
|
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
|
-
# @
|
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
|
-
#
|
190
|
-
# # ....
|
191
|
-
# end
|
130
|
+
# input none
|
192
131
|
#
|
193
|
-
# #
|
194
|
-
#
|
132
|
+
# # same as
|
133
|
+
# output Teckel::Contracts::None
|
195
134
|
#
|
196
|
-
#
|
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
|
-
#
|
240
|
-
#
|
241
|
-
#
|
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
|
-
#
|
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
|
-
#
|
269
|
-
|
270
|
-
|
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
|
-
# @!
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
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
|
-
|
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
|
-
|
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
|