teckel 0.3.0 → 0.4.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 +53 -0
- data/LICENSE_LOGO +4 -0
- data/README.md +4 -4
- data/lib/teckel.rb +9 -3
- data/lib/teckel/chain.rb +99 -271
- data/lib/teckel/chain/result.rb +38 -0
- data/lib/teckel/chain/runner.rb +51 -0
- data/lib/teckel/chain/step.rb +18 -0
- data/lib/teckel/config.rb +1 -23
- data/lib/teckel/contracts.rb +19 -0
- data/lib/teckel/operation.rb +309 -215
- data/lib/teckel/operation/result.rb +92 -0
- data/lib/teckel/operation/runner.rb +70 -0
- data/lib/teckel/result.rb +52 -53
- data/lib/teckel/version.rb +1 -1
- data/spec/chain/inheritance_spec.rb +116 -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/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 +485 -0
- data/spec/rb27/pattern_matching_spec.rb +193 -0
- data/spec/result_spec.rb +20 -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 +52 -25
- 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,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Teckel
|
6
|
+
module Chain
|
7
|
+
class Result < Teckel::Operation::Result
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
# @param value [Object] The result value
|
11
|
+
# @param success [Boolean] whether this is a successful result
|
12
|
+
# @param step [Teckel::Chain::Step]
|
13
|
+
def initialize(value, success, step)
|
14
|
+
super(value, success)
|
15
|
+
@step = step
|
16
|
+
end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
alias :[] :new
|
20
|
+
end
|
21
|
+
|
22
|
+
# @!method step
|
23
|
+
# Delegates to +step.name+
|
24
|
+
# @return [String,Symbol] The step name of the failed operation.
|
25
|
+
def_delegator :@step, :name, :step
|
26
|
+
|
27
|
+
def deconstruct
|
28
|
+
[successful?, @step.name, value]
|
29
|
+
end
|
30
|
+
|
31
|
+
def deconstruct_keys(keys)
|
32
|
+
super.tap { |e|
|
33
|
+
e[:step] = @step.name if keys.include?(:step)
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Teckel
|
4
|
+
module Chain
|
5
|
+
# The default implementation for executing a {Chain}
|
6
|
+
#
|
7
|
+
# @!visibility protected
|
8
|
+
class Runner
|
9
|
+
# @!visibility private
|
10
|
+
UNDEFINED = Object.new
|
11
|
+
|
12
|
+
def initialize(chain, settings = UNDEFINED)
|
13
|
+
@chain, @settings = chain, settings
|
14
|
+
end
|
15
|
+
attr_reader :chain, :settings
|
16
|
+
|
17
|
+
# Run steps
|
18
|
+
#
|
19
|
+
# @param input Any form of input the first steps +input+ class can handle
|
20
|
+
#
|
21
|
+
# @return [Teckel::Chain::Result] The result object wrapping
|
22
|
+
# either the success or failure value.
|
23
|
+
def call(input)
|
24
|
+
last_result = nil
|
25
|
+
last_step = nil
|
26
|
+
steps.each do |step|
|
27
|
+
last_step = step
|
28
|
+
value = last_result ? last_result.value : input
|
29
|
+
|
30
|
+
last_result = step.operation.call(value)
|
31
|
+
|
32
|
+
break if last_result.failure?
|
33
|
+
end
|
34
|
+
|
35
|
+
chain.result_constructor.call(last_result.value, last_result.successful?, last_step)
|
36
|
+
end
|
37
|
+
|
38
|
+
def steps
|
39
|
+
if settings == UNDEFINED
|
40
|
+
chain.steps
|
41
|
+
else
|
42
|
+
Enumerator.new do |yielder|
|
43
|
+
chain.steps.each do |step|
|
44
|
+
yielder << (settings.key?(step.name) ? step.with(settings[step.name]) : step)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Teckel
|
4
|
+
module Chain
|
5
|
+
# Internal wrapper of a step definition
|
6
|
+
Step = Struct.new(:name, :operation) do
|
7
|
+
def finalize!
|
8
|
+
name.freeze
|
9
|
+
operation.finalize!
|
10
|
+
freeze
|
11
|
+
end
|
12
|
+
|
13
|
+
def with(settings)
|
14
|
+
self.class.new(name, operation.with(settings))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/teckel/config.rb
CHANGED
@@ -2,29 +2,6 @@
|
|
2
2
|
|
3
3
|
module Teckel
|
4
4
|
class Config
|
5
|
-
@default_constructor = :[]
|
6
|
-
class << self
|
7
|
-
# @!attribute [r] default_constructor()
|
8
|
-
# The default constructor method for +input+, +output+ and +error+ class (default: +:[]+)
|
9
|
-
# @return [Class] The Output class
|
10
|
-
|
11
|
-
# @!method default_constructor(sym_or_proc)
|
12
|
-
# Set the default constructor method for +input+, +output+ and +error+ class
|
13
|
-
#
|
14
|
-
# defaults to +:[]+
|
15
|
-
#
|
16
|
-
# @param sym_or_proc [Symbol,#call] The method name on the +input+,
|
17
|
-
# +output+ and +error+ class or a callable which accepts the
|
18
|
-
# +input+, +output+ or +error+
|
19
|
-
#
|
20
|
-
# @return [Symbol,#call]
|
21
|
-
def default_constructor(sym_or_proc = nil)
|
22
|
-
return @default_constructor if sym_or_proc.nil?
|
23
|
-
|
24
|
-
@default_constructor = sym_or_proc
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
5
|
# @!visibility private
|
29
6
|
def initialize
|
30
7
|
@config = {}
|
@@ -38,6 +15,7 @@ module Teckel
|
|
38
15
|
# - sets (and returns) the blocks return value otherwise
|
39
16
|
# - calling without +value+ and +block+ works like {Hash#[]}
|
40
17
|
#
|
18
|
+
# @raise [FrozenConfigError] When overwriting a key
|
41
19
|
# @!visibility private
|
42
20
|
def for(key, value = nil, &block)
|
43
21
|
if value.nil?
|
@@ -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,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "operation/result"
|
4
|
+
require_relative "operation/runner"
|
5
|
+
|
3
6
|
module Teckel
|
4
7
|
# The main operation Mixin
|
5
8
|
#
|
@@ -10,7 +13,7 @@ module Teckel
|
|
10
13
|
# +input+. +output+ and +error+ methods to point them to anonymous classes.
|
11
14
|
#
|
12
15
|
# If you like "traditional" result objects to ask +successful?+ or +failure?+ on,
|
13
|
-
#
|
16
|
+
# use {.result!} and get {Teckel::Operation::Result}
|
14
17
|
#
|
15
18
|
# By default, +input+. +output+ and +error+ classes are build using +:[]+
|
16
19
|
# (eg: +Input[some: :param]+).
|
@@ -18,43 +21,6 @@ module Teckel
|
|
18
21
|
# {ClassMethods#output_constructor output_constructor} and
|
19
22
|
# {ClassMethods#error_constructor error_constructor} to change them.
|
20
23
|
#
|
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
|
-
#
|
56
|
-
# CreateUserViaConstants.call(name: "Bob", age: 23).is_a?(User) #=> true
|
57
|
-
#
|
58
24
|
# @example class definitions via methods
|
59
25
|
# class CreateUserViaMethods
|
60
26
|
# include Teckel::Operation
|
@@ -89,201 +55,272 @@ module Teckel
|
|
89
55
|
#
|
90
56
|
# @!visibility public
|
91
57
|
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
58
|
module ClassMethods
|
139
|
-
# @!
|
140
|
-
|
141
|
-
# @
|
142
|
-
|
143
|
-
#
|
144
|
-
#
|
145
|
-
#
|
146
|
-
#
|
59
|
+
# @!group Contacts definition
|
60
|
+
|
61
|
+
# @overload input()
|
62
|
+
# Get the configured class wrapping the input data structure.
|
63
|
+
# @return [Class] The +input+ class
|
64
|
+
# @overload input(klass)
|
65
|
+
# Set the class wrapping the input data structure.
|
66
|
+
# @param klass [Class] The +input+ class
|
67
|
+
# @return [Class] The +input+ class
|
147
68
|
def input(klass = nil)
|
148
69
|
@config.for(:input, klass) { self::Input if const_defined?(:Input) } ||
|
149
70
|
raise(Teckel::MissingConfigError, "Missing input config for #{self}")
|
150
71
|
end
|
151
72
|
|
152
|
-
#
|
153
|
-
#
|
154
|
-
#
|
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
|
73
|
+
# @overload input_constructor()
|
74
|
+
# The callable constructor to build an instance of the +input+ class.
|
75
|
+
# Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
|
76
|
+
# @return [Proc] A callable that will return an instance of the +input+ class.
|
162
77
|
#
|
163
|
-
# @
|
164
|
-
#
|
165
|
-
#
|
78
|
+
# @overload input_constructor(sym_or_proc)
|
79
|
+
# Define how to build the +input+.
|
80
|
+
# @param sym_or_proc [Symbol, #call]
|
81
|
+
# - Either a +Symbol+ representing the _public_ method to call on the +input+ class.
|
82
|
+
# - Or anything that response to +#call+ (like a +Proc+).
|
83
|
+
# @return [#call] The callable constructor
|
166
84
|
#
|
167
|
-
#
|
168
|
-
#
|
85
|
+
# @example simple symbol to method constructor
|
86
|
+
# class MyOperation
|
87
|
+
# include Teckel::Operation
|
88
|
+
#
|
89
|
+
# class Input
|
90
|
+
# def initialize(name:, age:); end
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# # If you need more control over how to build a new +Input+ instance
|
94
|
+
# # MyOperation.call(name: "Bob", age: 23) # -> Input.new(name: "Bob", age: 23)
|
95
|
+
# input_constructor :new
|
169
96
|
# end
|
170
97
|
#
|
171
|
-
#
|
172
|
-
# # MyOperation.call(name: "Bob", age: 23) # -> Input.new(name: "Bob", age: 23)
|
173
|
-
# input_constructor :new
|
174
|
-
# end
|
98
|
+
# MyOperation.input_constructor.is_a?(Method) #=> true
|
175
99
|
#
|
176
|
-
#
|
100
|
+
# @example Custom Proc constructor
|
101
|
+
# class MyOperation
|
102
|
+
# include Teckel::Operation
|
177
103
|
#
|
178
|
-
#
|
179
|
-
#
|
180
|
-
#
|
104
|
+
# class Input
|
105
|
+
# def initialize(*args, **opts); end
|
106
|
+
# end
|
181
107
|
#
|
182
|
-
#
|
183
|
-
#
|
108
|
+
# # If you need more control over how to build a new +Input+ instance
|
109
|
+
# # MyOperation.call("foo", opt: "bar") # -> Input.new(name: "foo", opt: "bar")
|
110
|
+
# input_constructor ->(name, options) { Input.new(name: name, **options) }
|
184
111
|
# end
|
185
112
|
#
|
186
|
-
#
|
187
|
-
|
188
|
-
|
189
|
-
# end
|
190
|
-
#
|
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) } ||
|
113
|
+
# MyOperation.input_constructor.is_a?(Proc) #=> true
|
114
|
+
def input_constructor(sym_or_proc = nil)
|
115
|
+
get_set_counstructor(:input_constructor, input, sym_or_proc) ||
|
194
116
|
raise(MissingConfigError, "Missing input_constructor config for #{self}")
|
195
117
|
end
|
196
118
|
|
197
|
-
#
|
198
|
-
#
|
199
|
-
#
|
200
|
-
|
201
|
-
#
|
202
|
-
#
|
203
|
-
#
|
204
|
-
#
|
119
|
+
# @overload output()
|
120
|
+
# Get the configured class wrapping the output data structure.
|
121
|
+
# Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
|
122
|
+
# @return [Class] The +output+ class
|
123
|
+
#
|
124
|
+
# @overload output(klass)
|
125
|
+
# Set the class wrapping the output data structure.
|
126
|
+
# @param klass [Class] The +output+ class
|
127
|
+
# @return [Class] The +output+ class
|
205
128
|
def output(klass = nil)
|
206
129
|
@config.for(:output, klass) { self::Output if const_defined?(:Output) } ||
|
207
130
|
raise(Teckel::MissingConfigError, "Missing output config for #{self}")
|
208
131
|
end
|
209
132
|
|
210
|
-
#
|
211
|
-
#
|
212
|
-
#
|
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
|
133
|
+
# @overload output_constructor()
|
134
|
+
# The callable constructor to build an instance of the +output+ class.
|
135
|
+
# Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
|
136
|
+
# @return [Proc] A callable that will return an instance of +output+ class.
|
220
137
|
#
|
221
|
-
# @
|
222
|
-
#
|
223
|
-
#
|
138
|
+
# @overload output_constructor(sym_or_proc)
|
139
|
+
# Define how to build the +output+.
|
140
|
+
# @param sym_or_proc [Symbol, #call]
|
141
|
+
# - Either a +Symbol+ representing the _public_ method to call on the +output+ class.
|
142
|
+
# - Or anything that response to +#call+ (like a +Proc+).
|
143
|
+
# @return [#call] The callable constructor
|
224
144
|
#
|
225
|
-
#
|
226
|
-
#
|
227
|
-
#
|
145
|
+
# @example
|
146
|
+
# class MyOperation
|
147
|
+
# include Teckel::Operation
|
228
148
|
#
|
229
|
-
#
|
230
|
-
#
|
149
|
+
# class Output
|
150
|
+
# def initialize(*args, **opts); end
|
151
|
+
# end
|
231
152
|
#
|
232
|
-
#
|
233
|
-
#
|
234
|
-
#
|
235
|
-
#
|
236
|
-
|
237
|
-
|
153
|
+
# # MyOperation.call("foo", "bar") # -> Output.new("foo", "bar")
|
154
|
+
# output_constructor :new
|
155
|
+
#
|
156
|
+
# # If you need more control over how to build a new +Output+ instance
|
157
|
+
# # MyOperation.call("foo", opt: "bar") # -> Output.new(name: "foo", opt: "bar")
|
158
|
+
# output_constructor ->(name, options) { Output.new(name: name, **options) }
|
159
|
+
# end
|
160
|
+
def output_constructor(sym_or_proc = nil)
|
161
|
+
get_set_counstructor(:output_constructor, output, sym_or_proc) ||
|
238
162
|
raise(MissingConfigError, "Missing output_constructor config for #{self}")
|
239
163
|
end
|
240
164
|
|
241
|
-
#
|
242
|
-
#
|
243
|
-
#
|
244
|
-
|
245
|
-
#
|
246
|
-
#
|
247
|
-
#
|
248
|
-
#
|
165
|
+
# @overload error()
|
166
|
+
# Get the configured class wrapping the error data structure.
|
167
|
+
# @return [Class] The +error+ class
|
168
|
+
#
|
169
|
+
# @overload error(klass)
|
170
|
+
# Set the class wrapping the error data structure.
|
171
|
+
# @param klass [Class] The +error+ class
|
172
|
+
# @return [Class,nil] The +error+ class or +nil+ if it does not error
|
249
173
|
def error(klass = nil)
|
250
174
|
@config.for(:error, klass) { self::Error if const_defined?(:Error) } ||
|
251
175
|
raise(Teckel::MissingConfigError, "Missing error config for #{self}")
|
252
176
|
end
|
253
177
|
|
254
|
-
#
|
255
|
-
#
|
256
|
-
#
|
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
|
178
|
+
# @overload error_constructor()
|
179
|
+
# The callable constructor to build an instance of the +error+ class.
|
180
|
+
# Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
|
181
|
+
# @return [Proc] A callable that will return an instance of +error+ class.
|
264
182
|
#
|
265
|
-
# @
|
266
|
-
#
|
267
|
-
#
|
183
|
+
# @overload error_constructor(sym_or_proc)
|
184
|
+
# Define how to build the +error+.
|
185
|
+
# @param sym_or_proc [Symbol, #call]
|
186
|
+
# - Either a +Symbol+ representing the _public_ method to call on the +error+ class.
|
187
|
+
# - Or anything that response to +#call+ (like a +Proc+).
|
188
|
+
# @return [#call] The callable constructor
|
268
189
|
#
|
269
|
-
#
|
270
|
-
#
|
271
|
-
#
|
190
|
+
# @example
|
191
|
+
# class MyOperation
|
192
|
+
# include Teckel::Operation
|
272
193
|
#
|
273
|
-
#
|
274
|
-
#
|
194
|
+
# class Error
|
195
|
+
# def initialize(*args, **opts); end
|
196
|
+
# end
|
275
197
|
#
|
276
|
-
#
|
277
|
-
#
|
278
|
-
#
|
279
|
-
#
|
280
|
-
|
281
|
-
|
198
|
+
# # MyOperation.call("foo", "bar") # -> Error.new("foo", "bar")
|
199
|
+
# error_constructor :new
|
200
|
+
#
|
201
|
+
# # If you need more control over how to build a new +Error+ instance
|
202
|
+
# # MyOperation.call("foo", opt: "bar") # -> Error.new(name: "foo", opt: "bar")
|
203
|
+
# error_constructor ->(name, options) { Error.new(name: name, **options) }
|
204
|
+
# end
|
205
|
+
def error_constructor(sym_or_proc = nil)
|
206
|
+
get_set_counstructor(:error_constructor, error, sym_or_proc) ||
|
282
207
|
raise(MissingConfigError, "Missing error_constructor config for #{self}")
|
283
208
|
end
|
284
209
|
|
285
|
-
#
|
286
|
-
|
210
|
+
# @!endgroup
|
211
|
+
|
212
|
+
# @overload settings()
|
213
|
+
# Get the configured class wrapping the settings data structure.
|
214
|
+
# @return [Class] The +settings+ class, or {Teckel::Contracts::None} as default
|
215
|
+
#
|
216
|
+
# @overload settings(klass)
|
217
|
+
# Set the class wrapping the settings data structure.
|
218
|
+
# @param klass [Class] The +settings+ class
|
219
|
+
# @return [Class] The +settings+ class configured
|
220
|
+
def settings(klass = nil)
|
221
|
+
@config.for(:settings, klass) { const_defined?(:Settings) ? self::Settings : none }
|
222
|
+
end
|
223
|
+
|
224
|
+
# @overload settings_constructor()
|
225
|
+
# The callable constructor to build an instance of the +settings+ class.
|
226
|
+
# Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
|
227
|
+
# @return [Proc] A callable that will return an instance of +settings+ class.
|
228
|
+
#
|
229
|
+
# @overload settings_constructor(sym_or_proc)
|
230
|
+
# Define how to build the +settings+.
|
231
|
+
# @param sym_or_proc [Symbol, #call]
|
232
|
+
# - Either a +Symbol+ representing the _public_ method to call on the +settings+ class.
|
233
|
+
# - Or anything that response to +#call+ (like a +Proc+).
|
234
|
+
# @return [#call] The callable constructor
|
235
|
+
#
|
236
|
+
# @example
|
237
|
+
# class MyOperation
|
238
|
+
# include Teckel::Operation
|
239
|
+
#
|
240
|
+
# class Settings
|
241
|
+
# def initialize(*args); end
|
242
|
+
# end
|
243
|
+
#
|
244
|
+
# # MyOperation.with("foo", "bar") # -> Settings.new("foo", "bar")
|
245
|
+
# settings_constructor :new
|
246
|
+
# end
|
247
|
+
def settings_constructor(sym_or_proc = nil)
|
248
|
+
get_set_counstructor(:settings_constructor, settings, sym_or_proc) ||
|
249
|
+
raise(MissingConfigError, "Missing settings_constructor config for #{self}")
|
250
|
+
end
|
251
|
+
|
252
|
+
# @overload runner()
|
253
|
+
# @return [Class] The Runner class
|
254
|
+
# @!visibility protected
|
255
|
+
#
|
256
|
+
# @overload runner(klass)
|
257
|
+
# Overwrite the default runner
|
258
|
+
# @param klass [Class] A class like the {Runner}
|
259
|
+
# @!visibility protected
|
260
|
+
def runner(klass = nil)
|
261
|
+
@config.for(:runner, klass) { Teckel::Operation::Runner }
|
262
|
+
end
|
263
|
+
|
264
|
+
# @overload result()
|
265
|
+
# Get the configured result object class wrapping {error} or {output}.
|
266
|
+
# The {ValueResult} default will act as a pass-through and does. Any error
|
267
|
+
# or output will just returned as-is.
|
268
|
+
# @return [Class] The +result+ class, or {ValueResult} as default
|
269
|
+
#
|
270
|
+
# @overload result(klass)
|
271
|
+
# Set the result object class wrapping {error} or {output}.
|
272
|
+
# @param klass [Class] The +result+ class
|
273
|
+
# @return [Class] The +result+ class configured
|
274
|
+
def result(klass = nil)
|
275
|
+
@config.for(:result, klass) { const_defined?(:Result, false) ? self::Result : ValueResult }
|
276
|
+
end
|
277
|
+
|
278
|
+
# @overload result_constructor()
|
279
|
+
# The callable constructor to build an instance of the +result+ class.
|
280
|
+
# Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
|
281
|
+
# @return [Proc] A callable that will return an instance of +result+ class.
|
282
|
+
#
|
283
|
+
# @overload result_constructor(sym_or_proc)
|
284
|
+
# Define how to build the +result+.
|
285
|
+
# @param sym_or_proc [Symbol, #call]
|
286
|
+
# - Either a +Symbol+ representing the _public_ method to call on the +result+ class.
|
287
|
+
# - Or anything that response to +#call+ (like a +Proc+).
|
288
|
+
# @return [#call] The callable constructor
|
289
|
+
#
|
290
|
+
# @example
|
291
|
+
# class MyOperation
|
292
|
+
# include Teckel::Operation
|
293
|
+
#
|
294
|
+
# class Result
|
295
|
+
# include Teckel::Result
|
296
|
+
# def initialize(value, success, opts = {}); end
|
297
|
+
# end
|
298
|
+
#
|
299
|
+
# # If you need more control over how to build a new +Settings+ instance
|
300
|
+
# result_constructor ->(value, success) { result.new(value, success, {foo: :bar}) }
|
301
|
+
# end
|
302
|
+
def result_constructor(sym_or_proc = nil)
|
303
|
+
get_set_counstructor(:result_constructor, result, sym_or_proc) ||
|
304
|
+
raise(MissingConfigError, "Missing result_constructor config for #{self}")
|
305
|
+
end
|
306
|
+
|
307
|
+
# @!group Shortcuts
|
308
|
+
|
309
|
+
# Shortcut to use {Teckel::Operation::Result} as a result object,
|
310
|
+
# wrapping any {error} or {output}.
|
311
|
+
#
|
312
|
+
# @!visibility protected
|
313
|
+
# @note Don't use in conjunction with {result} or {result_constructor}
|
314
|
+
# @return [nil]
|
315
|
+
def result!
|
316
|
+
@config.for(:result, Teckel::Operation::Result)
|
317
|
+
@config.for(:result_constructor, Teckel::Operation::Result.method(:new))
|
318
|
+
nil
|
319
|
+
end
|
320
|
+
|
321
|
+
# Convenience method for setting {#input}, {#output} or {#error} to the
|
322
|
+
# {Teckel::Contracts::None} value.
|
323
|
+
# @return [Object] The {Teckel::Contracts::None} class.
|
287
324
|
#
|
288
325
|
# @example Enforcing nil input, output or error
|
289
326
|
# class MyOperation
|
@@ -292,7 +329,7 @@ module Teckel
|
|
292
329
|
# input none
|
293
330
|
#
|
294
331
|
# # same as
|
295
|
-
# output Teckel::None
|
332
|
+
# output Teckel::Contracts::None
|
296
333
|
#
|
297
334
|
# error none
|
298
335
|
#
|
@@ -310,35 +347,71 @@ module Teckel
|
|
310
347
|
#
|
311
348
|
# MyOperation.call #=> nil
|
312
349
|
def none
|
313
|
-
None
|
350
|
+
Teckel::Contracts::None
|
314
351
|
end
|
315
352
|
|
316
|
-
#
|
317
|
-
# @return [Class] The Runner class
|
318
|
-
# @!visibility protected
|
319
|
-
|
320
|
-
# Overwrite the default runner
|
321
|
-
# @param klass [Class] A class like the {Runner}
|
322
|
-
# @!visibility protected
|
323
|
-
def runner(klass = nil)
|
324
|
-
@config.for(:runner, klass) { Runner }
|
325
|
-
end
|
353
|
+
# @endgroup
|
326
354
|
|
327
355
|
# Invoke the Operation
|
328
356
|
#
|
329
|
-
# @param input Any form of input your
|
330
|
-
# @return Either An instance of your defined
|
357
|
+
# @param input Any form of input your {#input} class can handle via the given {#input_constructor}
|
358
|
+
# @return Either An instance of your defined {#error} class or {#output} class
|
331
359
|
# @!visibility public
|
332
360
|
def call(input = nil)
|
333
361
|
runner.new(self).call(input)
|
334
362
|
end
|
335
363
|
|
364
|
+
# Provide {InstanceMethods#settings() settings} to the running operation.
|
365
|
+
#
|
366
|
+
# This method is intended to be called on the operation class outside of
|
367
|
+
# it's definition, prior to running {#call}.
|
368
|
+
#
|
369
|
+
# @param input Any form of input your {#settings} class can handle via the given {#settings_constructor}
|
370
|
+
# @return [Class] The configured {runner}
|
371
|
+
# @!visibility public
|
372
|
+
#
|
373
|
+
# @example Inject settings for an operation call
|
374
|
+
# LOG = []
|
375
|
+
#
|
376
|
+
# class MyOperation
|
377
|
+
# include ::Teckel::Operation
|
378
|
+
#
|
379
|
+
# settings Struct.new(:log)
|
380
|
+
#
|
381
|
+
# input none
|
382
|
+
# output none
|
383
|
+
# error none
|
384
|
+
#
|
385
|
+
# def call(_input)
|
386
|
+
# settings.log << "called" if settings&.log
|
387
|
+
# nil
|
388
|
+
# end
|
389
|
+
# end
|
390
|
+
#
|
391
|
+
# MyOperation.with(LOG).call
|
392
|
+
# LOG #=> ["called"]
|
393
|
+
#
|
394
|
+
# LOG.clear
|
395
|
+
#
|
396
|
+
# MyOperation.with(false).call
|
397
|
+
# MyOperation.call
|
398
|
+
# LOG #=> []
|
399
|
+
def with(input)
|
400
|
+
runner.new(self, settings_constructor.call(input))
|
401
|
+
end
|
402
|
+
alias :set :with
|
403
|
+
|
336
404
|
# @!visibility private
|
337
405
|
# @return [void]
|
338
406
|
def define!
|
339
|
-
%i[
|
340
|
-
|
341
|
-
|
407
|
+
%i[
|
408
|
+
input input_constructor
|
409
|
+
output output_constructor
|
410
|
+
error error_constructor
|
411
|
+
settings settings_constructor
|
412
|
+
result result_constructor
|
413
|
+
runner
|
414
|
+
].each { |e| public_send(e) }
|
342
415
|
nil
|
343
416
|
end
|
344
417
|
|
@@ -351,7 +424,7 @@ module Teckel
|
|
351
424
|
def finalize!
|
352
425
|
define!
|
353
426
|
@config.freeze
|
354
|
-
|
427
|
+
self
|
355
428
|
end
|
356
429
|
|
357
430
|
# Produces a shallow copy of this operation and all it's configuration.
|
@@ -378,8 +451,29 @@ module Teckel
|
|
378
451
|
end
|
379
452
|
end
|
380
453
|
|
454
|
+
# @!visibility private
|
455
|
+
def inherited(subclass)
|
456
|
+
subclass.instance_variable_set(:@config, @config.dup)
|
457
|
+
end
|
458
|
+
|
459
|
+
# @!visibility private
|
460
|
+
def self.extended(base)
|
461
|
+
base.instance_exec do
|
462
|
+
@config = Config.new
|
463
|
+
attr_accessor :settings
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
381
467
|
private
|
382
468
|
|
469
|
+
def get_set_counstructor(name, on, sym_or_proc)
|
470
|
+
constructor = build_counstructor(on, sym_or_proc) unless sym_or_proc.nil?
|
471
|
+
|
472
|
+
@config.for(name, constructor) {
|
473
|
+
build_counstructor(on, Teckel::DEFAULT_CONSTRUCTOR)
|
474
|
+
}
|
475
|
+
end
|
476
|
+
|
383
477
|
def build_counstructor(on, sym_or_proc)
|
384
478
|
if sym_or_proc.is_a?(Symbol) && on.respond_to?(sym_or_proc)
|
385
479
|
on.public_method(sym_or_proc)
|
@@ -390,6 +484,12 @@ module Teckel
|
|
390
484
|
end
|
391
485
|
|
392
486
|
module InstanceMethods
|
487
|
+
# @!attribute [r] settings()
|
488
|
+
# @return [Class,nil] When executed with settings, an instance of the
|
489
|
+
# configured {.settings} class. Otherwise +nil+
|
490
|
+
# @see ClassMethods#settings
|
491
|
+
# @!visibility public
|
492
|
+
|
393
493
|
# Halt any further execution with a output value
|
394
494
|
#
|
395
495
|
# @return a thing matching your {Operation::ClassMethods#output Operation#output} definition
|
@@ -410,12 +510,6 @@ module Teckel
|
|
410
510
|
def self.included(receiver)
|
411
511
|
receiver.extend ClassMethods
|
412
512
|
receiver.send :include, InstanceMethods
|
413
|
-
|
414
|
-
receiver.class_eval do
|
415
|
-
@config = Config.new
|
416
|
-
|
417
|
-
protected :success!, :fail!
|
418
|
-
end
|
419
513
|
end
|
420
514
|
end
|
421
515
|
end
|