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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +111 -0
- data/LICENSE_LOGO +4 -0
- data/README.md +4 -4
- data/lib/teckel.rb +9 -4
- data/lib/teckel/chain.rb +31 -341
- data/lib/teckel/chain/config.rb +275 -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 +25 -28
- data/lib/teckel/contracts.rb +19 -0
- data/lib/teckel/operation.rb +84 -302
- data/lib/teckel/operation/config.rb +396 -0
- data/lib/teckel/operation/result.rb +92 -0
- data/lib/teckel/operation/runner.rb +74 -0
- data/lib/teckel/result.rb +52 -53
- data/lib/teckel/version.rb +1 -1
- data/spec/chain/around_hook_spec.rb +100 -0
- 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_spec.rb +180 -0
- data/spec/config_spec.rb +26 -0
- data/spec/doctest_helper.rb +8 -0
- data/spec/operation/contract_trace_spec.rb +116 -0
- data/spec/operation/default_settings_spec.rb +94 -0
- data/spec/operation/fail_on_input_spec.rb +103 -0
- data/spec/operation/inheritance_spec.rb +94 -0
- data/spec/operation/result_spec.rb +55 -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 +28 -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 +68 -28
- data/.codeclimate.yml +0 -3
- data/.github/workflows/ci.yml +0 -92
- data/.github/workflows/pages.yml +0 -50
- data/.gitignore +0 -15
- data/.rspec +0 -3
- data/.rubocop.yml +0 -12
- data/.ruby-version +0 -1
- data/DEVELOPMENT.md +0 -32
- data/Gemfile +0 -16
- data/Rakefile +0 -35
- 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/none.rb +0 -18
- data/lib/teckel/operation/results.rb +0 -72
- 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
|
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
|
#
|
@@ -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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
140
|
-
#
|
141
|
-
# @
|
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
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
#
|
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
|
-
#
|
168
|
-
#
|
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
|
-
#
|
172
|
-
#
|
173
|
-
#
|
174
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
192
|
-
|
193
|
-
|
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
|
-
#
|
226
|
-
#
|
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
|
-
#
|
266
|
-
#
|
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
|
-
#
|
274
|
-
# error_constructor :new
|
109
|
+
# LOG.clear
|
275
110
|
#
|
276
|
-
#
|
277
|
-
#
|
278
|
-
#
|
279
|
-
|
280
|
-
|
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},
|
286
|
-
#
|
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
|
-
|
327
|
-
#
|
328
|
-
# @
|
329
|
-
# @
|
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
|
-
#
|
345
|
-
#
|
170
|
+
# Delegates to the configured Runner.
|
171
|
+
# The default behavior is to halt any further execution with a output value.
|
346
172
|
#
|
347
|
-
# @
|
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
|
-
|
176
|
+
runner.success!(*args)
|
391
177
|
end
|
392
178
|
|
393
|
-
#
|
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
|
-
|
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
|