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,246 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Teckel
|
4
|
+
module Chain
|
5
|
+
module Config
|
6
|
+
# Declare a {Operation} as a named step
|
7
|
+
#
|
8
|
+
# @param name [String,Symbol] The name of the operation.
|
9
|
+
# This name is used in an error case to let you know which step failed.
|
10
|
+
# @param operation [Operation] The operation to call, which
|
11
|
+
# must return a {Teckel::Result} object.
|
12
|
+
def step(name, operation)
|
13
|
+
steps << Step.new(name, operation)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Get the list of defined steps
|
17
|
+
#
|
18
|
+
# @return [<Step>]
|
19
|
+
def steps
|
20
|
+
@config.for(:steps) { [] }
|
21
|
+
end
|
22
|
+
|
23
|
+
# Set or get the optional around hook.
|
24
|
+
# A Hook might be given as a block or anything callable. The execution of
|
25
|
+
# the chain is yielded to this hook. The first argument being the callable
|
26
|
+
# chain ({Runner}) and the second argument the +input+ data. The hook also
|
27
|
+
# needs to return the result.
|
28
|
+
#
|
29
|
+
# @param callable [Proc,{#call}] The hook to pass chain execution control to. (nil)
|
30
|
+
#
|
31
|
+
# @return [Proc,{#call}] The configured hook
|
32
|
+
#
|
33
|
+
# @example Around hook with block
|
34
|
+
# OUTPUTS = []
|
35
|
+
#
|
36
|
+
# class Echo
|
37
|
+
# include ::Teckel::Operation
|
38
|
+
# result!
|
39
|
+
#
|
40
|
+
# input Hash
|
41
|
+
# output input
|
42
|
+
#
|
43
|
+
# def call(hsh)
|
44
|
+
# success!(hsh)
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# class MyChain
|
49
|
+
# include Teckel::Chain
|
50
|
+
#
|
51
|
+
# around do |chain, input|
|
52
|
+
# OUTPUTS << "before start"
|
53
|
+
# result = chain.call(input)
|
54
|
+
# OUTPUTS << "after start"
|
55
|
+
# result
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# step :noop, Echo
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# result = MyChain.call(some: 'test')
|
62
|
+
# OUTPUTS #=> ["before start", "after start"]
|
63
|
+
# result.success #=> { some: "test" }
|
64
|
+
def around(callable = nil, &block)
|
65
|
+
@config.for(:around, callable || block)
|
66
|
+
end
|
67
|
+
|
68
|
+
# @!attribute [r] runner()
|
69
|
+
# @return [Class] The Runner class
|
70
|
+
# @!visibility protected
|
71
|
+
|
72
|
+
# Overwrite the default runner
|
73
|
+
# @param klass [Class] A class like the {Runner}
|
74
|
+
# @!visibility protected
|
75
|
+
def runner(klass = nil)
|
76
|
+
@config.for(:runner, klass) { Runner }
|
77
|
+
end
|
78
|
+
|
79
|
+
# @overload result()
|
80
|
+
# Get the configured result object class wrapping {.error} or {.output}.
|
81
|
+
# @return [Class] The +result+ class, or {Teckel::Chain::Result} as default
|
82
|
+
#
|
83
|
+
# @overload result(klass)
|
84
|
+
# Set the result object class wrapping {.error} or {.output}.
|
85
|
+
# @param klass [Class] The +result+ class
|
86
|
+
# @return [Class] The +result+ class configured
|
87
|
+
def result(klass = nil)
|
88
|
+
@config.for(:result, klass) { const_defined?(:Result, false) ? self::Result : Teckel::Chain::Result }
|
89
|
+
end
|
90
|
+
|
91
|
+
# @overload result_constructor()
|
92
|
+
# The callable constructor to build an instance of the +result+ class.
|
93
|
+
# Defaults to {Teckel::DEFAULT_CONSTRUCTOR}
|
94
|
+
# @return [Proc] A callable that will return an instance of +result+ class.
|
95
|
+
#
|
96
|
+
# @overload result_constructor(sym_or_proc)
|
97
|
+
# Define how to build the +result+.
|
98
|
+
# @param sym_or_proc [Symbol, #call]
|
99
|
+
# - Either a +Symbol+ representing the _public_ method to call on the +result+ class.
|
100
|
+
# - Or anything that response to +#call+ (like a +Proc+).
|
101
|
+
# @return [#call] The callable constructor
|
102
|
+
#
|
103
|
+
# @example
|
104
|
+
# class MyOperation
|
105
|
+
# include Teckel::Operation
|
106
|
+
#
|
107
|
+
# class Result < Teckel::Operation::Result
|
108
|
+
# def initialize(value, success, step, options = {}); end
|
109
|
+
# end
|
110
|
+
#
|
111
|
+
# # If you need more control over how to build a new +Settings+ instance
|
112
|
+
# result_constructor ->(value, success, step) { result.new(value, success, step, {foo: :bar}) }
|
113
|
+
# end
|
114
|
+
def result_constructor(sym_or_proc = nil)
|
115
|
+
constructor = build_constructor(result, sym_or_proc) unless sym_or_proc.nil?
|
116
|
+
|
117
|
+
@config.for(:result_constructor, constructor) {
|
118
|
+
build_constructor(result, Teckel::DEFAULT_CONSTRUCTOR)
|
119
|
+
} || raise(MissingConfigError, "Missing result_constructor config for #{self}")
|
120
|
+
end
|
121
|
+
|
122
|
+
# Declare default settings operation iin this chain should use when called without
|
123
|
+
# {Teckel::Chain::ClassMethods#with #with}.
|
124
|
+
#
|
125
|
+
# Explicit call-time settings will *not* get merged with declared default setting.
|
126
|
+
#
|
127
|
+
# @param settings [Hash{String,Symbol => Object}] Set settings for a step by it's name
|
128
|
+
#
|
129
|
+
# @example
|
130
|
+
# class MyOperation
|
131
|
+
# include Teckel::Operation
|
132
|
+
# result!
|
133
|
+
#
|
134
|
+
# settings Struct.new(:say, :other)
|
135
|
+
# settings_constructor ->(data) { settings.new(*data.values_at(*settings.members)) }
|
136
|
+
#
|
137
|
+
# input none
|
138
|
+
# output Hash
|
139
|
+
# error none
|
140
|
+
#
|
141
|
+
# def call(_)
|
142
|
+
# success!(settings.to_h)
|
143
|
+
# end
|
144
|
+
# end
|
145
|
+
#
|
146
|
+
# class Chain
|
147
|
+
# include Teckel::Chain
|
148
|
+
#
|
149
|
+
# default_settings!(a: { say: "Chain Default" })
|
150
|
+
#
|
151
|
+
# step :a, MyOperation
|
152
|
+
# end
|
153
|
+
#
|
154
|
+
# # Using the chains default settings
|
155
|
+
# result = Chain.call
|
156
|
+
# result.success #=> {say: "Chain Default", other: nil}
|
157
|
+
#
|
158
|
+
# # explicit settings passed via `with` will overwrite all defaults
|
159
|
+
# result = Chain.with(a: { other: "What" }).call
|
160
|
+
# result.success #=> {say: nil, other: "What"}
|
161
|
+
def default_settings!(settings) # :nodoc: The bang is for consistency with the Operation class
|
162
|
+
@config.for(:default_settings, settings)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Getter for configured default settings
|
166
|
+
# @return [nil|#call] The callable constructor
|
167
|
+
def default_settings
|
168
|
+
@config.for(:default_settings)
|
169
|
+
end
|
170
|
+
|
171
|
+
REQUIRED_CONFIGS = %i[around runner result result_constructor].freeze
|
172
|
+
|
173
|
+
# @!visibility private
|
174
|
+
# @return [void]
|
175
|
+
def define!
|
176
|
+
raise MissingConfigError, "Cannot define Chain with no steps" if steps.empty?
|
177
|
+
|
178
|
+
REQUIRED_CONFIGS.each { |e| public_send(e) }
|
179
|
+
steps.each(&:finalize!)
|
180
|
+
nil
|
181
|
+
end
|
182
|
+
|
183
|
+
# Disallow any further changes to this Chain.
|
184
|
+
# @note This also calls +finalize!+ on all Operations defined as steps.
|
185
|
+
#
|
186
|
+
# @return [self] Frozen self
|
187
|
+
# @!visibility public
|
188
|
+
def finalize!
|
189
|
+
define!
|
190
|
+
steps.freeze
|
191
|
+
@config.freeze
|
192
|
+
self
|
193
|
+
end
|
194
|
+
|
195
|
+
# Produces a shallow copy of this chain.
|
196
|
+
# It's {around}, {runner} and {steps} will get +dup+'ed
|
197
|
+
#
|
198
|
+
# @return [self]
|
199
|
+
# @!visibility public
|
200
|
+
def dup
|
201
|
+
dup_config(super)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Produces a clone of this chain.
|
205
|
+
# It's {around}, {runner} and {steps} will get +dup+'ed
|
206
|
+
#
|
207
|
+
# @return [self]
|
208
|
+
# @!visibility public
|
209
|
+
def clone
|
210
|
+
if frozen?
|
211
|
+
super
|
212
|
+
else
|
213
|
+
dup_config(super)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# @!visibility private
|
218
|
+
def inherited(subclass)
|
219
|
+
super dup_config(subclass)
|
220
|
+
end
|
221
|
+
|
222
|
+
# @!visibility private
|
223
|
+
def self.extended(base)
|
224
|
+
base.instance_variable_set(:@config, Teckel::Config.new)
|
225
|
+
end
|
226
|
+
|
227
|
+
private
|
228
|
+
|
229
|
+
def dup_config(other_class)
|
230
|
+
new_config = @config.dup
|
231
|
+
new_config.replace(:steps) { steps.dup }
|
232
|
+
|
233
|
+
other_class.instance_variable_set(:@config, new_config)
|
234
|
+
other_class
|
235
|
+
end
|
236
|
+
|
237
|
+
def build_constructor(on, sym_or_proc)
|
238
|
+
if sym_or_proc.is_a?(Symbol) && on.respond_to?(sym_or_proc)
|
239
|
+
on.public_method(sym_or_proc)
|
240
|
+
elsif sym_or_proc.respond_to?(:call)
|
241
|
+
sym_or_proc
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
@@ -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
|
+
e = super
|
33
|
+
e[:step] = @step.name if keys.include?(:step)
|
34
|
+
e
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,62 @@
|
|
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
|
+
# @!visibility private
|
13
|
+
StepResult = Struct.new(:value, :success, :step)
|
14
|
+
|
15
|
+
def initialize(chain, settings = UNDEFINED)
|
16
|
+
@chain, @settings = chain, settings
|
17
|
+
end
|
18
|
+
attr_reader :chain, :settings
|
19
|
+
|
20
|
+
# Run steps
|
21
|
+
#
|
22
|
+
# @param input Any form of input the first steps +input+ class can handle
|
23
|
+
#
|
24
|
+
# @return [Teckel::Chain::Result] The result object wrapping
|
25
|
+
# either the success or failure value.
|
26
|
+
def call(input = nil)
|
27
|
+
step_result = run(input)
|
28
|
+
chain.result_constructor.call(*step_result)
|
29
|
+
end
|
30
|
+
|
31
|
+
def steps
|
32
|
+
settings == UNDEFINED ? chain.steps : steps_with_settings
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def run(input)
|
38
|
+
steps.each_with_object(StepResult.new(input)) do |step, step_result|
|
39
|
+
result = step.operation.call(step_result.value)
|
40
|
+
|
41
|
+
step_result.step = step
|
42
|
+
step_result.value = result.value
|
43
|
+
step_result.success = result.successful?
|
44
|
+
|
45
|
+
break step_result if result.failure?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def step_with_settings(step)
|
50
|
+
settings.key?(step.name) ? step.with(settings[step.name]) : step
|
51
|
+
end
|
52
|
+
|
53
|
+
def steps_with_settings
|
54
|
+
Enumerator.new do |yielder|
|
55
|
+
chain.steps.each do |step|
|
56
|
+
yielder << step_with_settings(step)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
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,68 +2,57 @@
|
|
2
2
|
|
3
3
|
module Teckel
|
4
4
|
class Config
|
5
|
-
|
6
|
-
|
7
|
-
@default_constructor = :[]
|
8
|
-
class << self
|
9
|
-
def default_constructor(sym_or_proc = nil)
|
10
|
-
return @default_constructor if sym_or_proc.nil?
|
11
|
-
|
12
|
-
@default_constructor = sym_or_proc
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
5
|
+
# @!visibility private
|
16
6
|
def initialize
|
17
|
-
@
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
7
|
+
@config = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
# Allow getting or setting a value, with some weird rules:
|
11
|
+
# - The +value+ might not be +nil+
|
12
|
+
# - Setting via +value+ is allowed only once. Successive calls will raise a {FrozenConfigError}
|
13
|
+
# - Setting via +block+ works almost like {Hash#fetch}:
|
14
|
+
# - returns the existing value if key is present
|
15
|
+
# - sets (and returns) the blocks return value otherwise
|
16
|
+
# - calling without +value+ and +block+ works like {Hash#[]}
|
17
|
+
#
|
18
|
+
# @raise [FrozenConfigError] When overwriting a key
|
19
|
+
# @!visibility private
|
20
|
+
def for(key, value = nil, &block)
|
21
|
+
if value.nil?
|
22
|
+
get_or_set(key, &block)
|
23
|
+
elsif @config.key?(key)
|
24
|
+
raise FrozenConfigError, "Configuration #{key} is already set"
|
25
|
+
else
|
26
|
+
@config[key] = value
|
27
|
+
end
|
32
28
|
end
|
33
29
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
@input_constructor = sym_or_proc
|
30
|
+
# @!visibility private
|
31
|
+
def replace(key)
|
32
|
+
@config[key] = yield if @config.key?(key)
|
39
33
|
end
|
40
34
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
@output_class = klass
|
35
|
+
# @!visibility private
|
36
|
+
def freeze
|
37
|
+
@config.freeze
|
38
|
+
super
|
46
39
|
end
|
47
40
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
end
|
54
|
-
|
55
|
-
def error(klass = nil)
|
56
|
-
return @error_class if klass.nil?
|
57
|
-
raise FrozenConfigError unless @error_class.nil?
|
58
|
-
|
59
|
-
@error_class = klass
|
41
|
+
# @!visibility private
|
42
|
+
def dup
|
43
|
+
super.tap do |copy|
|
44
|
+
copy.instance_variable_set(:@config, @config.dup)
|
45
|
+
end
|
60
46
|
end
|
61
47
|
|
62
|
-
|
63
|
-
return (@error_constructor || self.class.default_constructor) if sym_or_proc.nil?
|
64
|
-
raise FrozenConfigError unless @error_constructor.nil?
|
48
|
+
private
|
65
49
|
|
66
|
-
|
50
|
+
def get_or_set(key, &block)
|
51
|
+
if block
|
52
|
+
@config[key] ||= @config.fetch(key, &block)
|
53
|
+
else
|
54
|
+
@config[key]
|
55
|
+
end
|
67
56
|
end
|
68
57
|
end
|
69
58
|
end
|