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,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Teckel
|
4
|
+
module Operation
|
5
|
+
# The default implementation for executing a single {Operation}
|
6
|
+
# @note You shouldn't need to call this explicitly.
|
7
|
+
# Use {ClassMethods#with MyOperation.with()} or {ClassMethods#with MyOperation.call()} instead.
|
8
|
+
# @!visibility protected
|
9
|
+
class Runner
|
10
|
+
# @!visibility private
|
11
|
+
UNDEFINED = Object.new.freeze
|
12
|
+
|
13
|
+
def initialize(operation, settings = UNDEFINED)
|
14
|
+
@operation, @settings = operation, settings
|
15
|
+
end
|
16
|
+
attr_reader :operation, :settings
|
17
|
+
|
18
|
+
def call(input = nil)
|
19
|
+
catch(:failure) do
|
20
|
+
out = catch(:success) do
|
21
|
+
run operation.input_constructor.call(input)
|
22
|
+
return nil # :sic!: return values need to go through +success!+
|
23
|
+
end
|
24
|
+
|
25
|
+
return out
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# This is just here to raise a meaningful error.
|
30
|
+
# @!visibility private
|
31
|
+
def with(*)
|
32
|
+
raise Teckel::Error, "Operation already has settings assigned."
|
33
|
+
end
|
34
|
+
|
35
|
+
# Halt any further execution with a output value
|
36
|
+
#
|
37
|
+
# @return a thing matching your {Teckel::Operation::Config#output output} definition
|
38
|
+
# @!visibility protected
|
39
|
+
def success!(*args)
|
40
|
+
value =
|
41
|
+
if args.size == 1 && operation.output === args.first # rubocop:disable Style/CaseEquality
|
42
|
+
args.first
|
43
|
+
else
|
44
|
+
operation.output_constructor.call(*args)
|
45
|
+
end
|
46
|
+
|
47
|
+
throw :success, operation.result_constructor.call(value, true)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Halt any further execution with an error value
|
51
|
+
#
|
52
|
+
# @return a thing matching your {Teckel::Operation::Config#error error} definition
|
53
|
+
# @!visibility protected
|
54
|
+
def fail!(*args)
|
55
|
+
value =
|
56
|
+
if args.size == 1 && operation.error === args.first # rubocop:disable Style/CaseEquality
|
57
|
+
args.first
|
58
|
+
else
|
59
|
+
operation.error_constructor.call(*args)
|
60
|
+
end
|
61
|
+
|
62
|
+
throw :failure, operation.result_constructor.call(value, false)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def run(input)
|
68
|
+
op = @operation.new
|
69
|
+
op.runner = self
|
70
|
+
op.settings = settings if settings != UNDEFINED
|
71
|
+
op.call(input)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/teckel/result.rb
CHANGED
@@ -1,63 +1,62 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Teckel
|
4
|
-
#
|
5
|
-
#
|
6
|
-
# @example
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
# Teckel::Result.new("some error", false).success("other default") #=> "other default"
|
25
|
-
# Teckel::Result.new("some error", false).success { |value| "Failed: #{value}" } #=> "Failed: some error"
|
26
|
-
#
|
27
|
-
# @api public
|
28
|
-
class Result
|
29
|
-
# @param value [Mixed] the value/payload of the result.
|
30
|
-
# @param success [Bool] whether this is a successful result
|
31
|
-
def initialize(value, success)
|
32
|
-
@value = value
|
33
|
-
@success = success
|
4
|
+
# @abstract The interface an {Operation}s result object needs to adopt.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# class MyResult
|
8
|
+
# include Teckel::Result
|
9
|
+
#
|
10
|
+
# def initialize(value, success)
|
11
|
+
# @value = value
|
12
|
+
# @success = (!!success).freeze
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# def successful?; @success end
|
16
|
+
#
|
17
|
+
# def value; @value end
|
18
|
+
# end
|
19
|
+
module Result
|
20
|
+
module ClassMethods
|
21
|
+
def [](value, success)
|
22
|
+
new(value, success)
|
23
|
+
end
|
34
24
|
end
|
35
25
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
26
|
+
module InstanceMethods
|
27
|
+
# Whether this is a success result
|
28
|
+
# @return [Boolean]
|
29
|
+
def successful?
|
30
|
+
raise NotImplementedError, "Result object does not implement `successful?`"
|
31
|
+
end
|
32
|
+
|
33
|
+
# Whether this is a error/failure result
|
34
|
+
# @return [Boolean]
|
35
|
+
def failure?
|
36
|
+
!successful?
|
37
|
+
end
|
38
|
+
|
39
|
+
# @!attribute [r] value
|
40
|
+
# @return [Mixed] the value/payload
|
41
|
+
def value
|
42
|
+
raise NotImplementedError, "Result object does not implement `value`"
|
43
|
+
end
|
44
|
+
|
45
|
+
def deconstruct
|
46
|
+
[successful?, value]
|
47
|
+
end
|
48
|
+
|
49
|
+
def deconstruct_keys(keys)
|
50
|
+
e = {}
|
51
|
+
e[:success] = successful? if keys.include?(:success)
|
52
|
+
e[:value] = value if keys.include?(:value)
|
53
|
+
e
|
54
|
+
end
|
53
55
|
end
|
54
56
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
return yield(@value) if block
|
59
|
-
|
60
|
-
default
|
57
|
+
def self.included(receiver)
|
58
|
+
receiver.extend ClassMethods
|
59
|
+
receiver.send :include, InstanceMethods
|
61
60
|
end
|
62
61
|
end
|
63
62
|
end
|
data/lib/teckel/version.rb
CHANGED
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TeckelChainDefaultSettingsTest
|
4
|
+
class MyOperation
|
5
|
+
include Teckel::Operation
|
6
|
+
result!
|
7
|
+
|
8
|
+
settings Struct.new(:say, :other)
|
9
|
+
settings_constructor ->(data) { settings.new(*data.values_at(*settings.members)) } # ruby 2.4 way for `keyword_init: true`
|
10
|
+
|
11
|
+
input none
|
12
|
+
output Hash
|
13
|
+
error none
|
14
|
+
|
15
|
+
def call(_)
|
16
|
+
success! settings.to_h
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Chain
|
21
|
+
include Teckel::Chain
|
22
|
+
|
23
|
+
default_settings!(a: { say: "Chain Default" })
|
24
|
+
|
25
|
+
step :a, MyOperation
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
RSpec.describe Teckel::Chain do
|
30
|
+
specify "call chain without settings, uses default settings" do
|
31
|
+
result = TeckelChainDefaultSettingsTest::Chain.call
|
32
|
+
expect(result.success).to eq(say: "Chain Default", other: nil)
|
33
|
+
end
|
34
|
+
|
35
|
+
specify "call chain with explicit settings, overwrites defaults" do
|
36
|
+
result = TeckelChainDefaultSettingsTest::Chain.with(a: { other: "What" }).call
|
37
|
+
expect(result.success).to eq(say: nil, other: "What")
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'support/dry_base'
|
4
|
+
require 'support/fake_models'
|
5
|
+
|
6
|
+
module TeckelChainDefaultsViaBaseClass
|
7
|
+
LOG = [] # rubocop:disable Style/MutableConstant
|
8
|
+
|
9
|
+
class LoggingChain
|
10
|
+
include Teckel::Chain
|
11
|
+
|
12
|
+
around do |chain, input|
|
13
|
+
require 'benchmark'
|
14
|
+
result = nil
|
15
|
+
LOG << Benchmark.measure { result = chain.call(input) }
|
16
|
+
result
|
17
|
+
end
|
18
|
+
|
19
|
+
freeze
|
20
|
+
end
|
21
|
+
|
22
|
+
class OperationA
|
23
|
+
include Teckel::Operation
|
24
|
+
|
25
|
+
result!
|
26
|
+
|
27
|
+
input none
|
28
|
+
output Types::Integer
|
29
|
+
error none
|
30
|
+
|
31
|
+
def call(_)
|
32
|
+
success! rand(1000)
|
33
|
+
end
|
34
|
+
|
35
|
+
finalize!
|
36
|
+
end
|
37
|
+
|
38
|
+
class OperationB
|
39
|
+
include Teckel::Operation
|
40
|
+
|
41
|
+
result!
|
42
|
+
|
43
|
+
input none
|
44
|
+
output Types::String
|
45
|
+
error none
|
46
|
+
|
47
|
+
def call(_)
|
48
|
+
success! ("a".."z").to_a.sample
|
49
|
+
end
|
50
|
+
|
51
|
+
finalize!
|
52
|
+
end
|
53
|
+
|
54
|
+
class ChainA < LoggingChain
|
55
|
+
step :roll, OperationA
|
56
|
+
|
57
|
+
finalize!
|
58
|
+
end
|
59
|
+
|
60
|
+
class ChainB < LoggingChain
|
61
|
+
step :say, OperationB
|
62
|
+
|
63
|
+
finalize!
|
64
|
+
end
|
65
|
+
|
66
|
+
class ChainC < ChainB
|
67
|
+
finalize!
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
RSpec.describe Teckel::Chain do
|
72
|
+
before do
|
73
|
+
TeckelChainDefaultsViaBaseClass::LOG.clear
|
74
|
+
end
|
75
|
+
|
76
|
+
let(:base_chain) { TeckelChainDefaultsViaBaseClass::LoggingChain }
|
77
|
+
let(:chain_a) { TeckelChainDefaultsViaBaseClass::ChainA }
|
78
|
+
let(:chain_b) { TeckelChainDefaultsViaBaseClass::ChainB }
|
79
|
+
let(:chain_c) { TeckelChainDefaultsViaBaseClass::ChainC }
|
80
|
+
|
81
|
+
it "inherits config" do
|
82
|
+
expect(chain_a.around)
|
83
|
+
expect(chain_b.around)
|
84
|
+
|
85
|
+
expect(base_chain.steps).to eq([])
|
86
|
+
expect(chain_a.steps.size).to eq(1)
|
87
|
+
expect(chain_b.steps.size).to eq(1)
|
88
|
+
end
|
89
|
+
|
90
|
+
it "runs chain a" do
|
91
|
+
expect {
|
92
|
+
result = chain_a.call
|
93
|
+
expect(result.success).to be_a(Integer)
|
94
|
+
}.to change {
|
95
|
+
TeckelChainDefaultsViaBaseClass::LOG.size
|
96
|
+
}.from(0).to(1)
|
97
|
+
end
|
98
|
+
|
99
|
+
it "runs chain b" do
|
100
|
+
expect {
|
101
|
+
result = chain_b.call
|
102
|
+
expect(result.success).to be_a(String)
|
103
|
+
}.to change {
|
104
|
+
TeckelChainDefaultsViaBaseClass::LOG.size
|
105
|
+
}.from(0).to(1)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "inherits steps" do
|
109
|
+
expect {
|
110
|
+
result = chain_c.call
|
111
|
+
expect(result.success).to be_a(String)
|
112
|
+
}.to change {
|
113
|
+
TeckelChainDefaultsViaBaseClass::LOG.size
|
114
|
+
}.from(0).to(1)
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TeckelChainNoneInputTest
|
4
|
+
class MyOperation
|
5
|
+
include Teckel::Operation
|
6
|
+
result!
|
7
|
+
|
8
|
+
settings Struct.new(:say)
|
9
|
+
|
10
|
+
input none
|
11
|
+
output String
|
12
|
+
error none
|
13
|
+
|
14
|
+
def call(_)
|
15
|
+
success!(settings&.say || "Called")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Chain
|
20
|
+
include Teckel::Chain
|
21
|
+
|
22
|
+
step :a, MyOperation
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
RSpec.describe Teckel::Chain do
|
27
|
+
specify "call chain without input value" do
|
28
|
+
result = TeckelChainNoneInputTest::Chain.call
|
29
|
+
expect(result.success).to eq("Called")
|
30
|
+
end
|
31
|
+
|
32
|
+
specify "call chain runner without input value" do
|
33
|
+
result = TeckelChainNoneInputTest::Chain.with(a: "What").call
|
34
|
+
expect(result.success).to eq("What")
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'support/dry_base'
|
4
|
+
require 'support/fake_models'
|
5
|
+
|
6
|
+
module TeckelChainResultTest
|
7
|
+
class Message
|
8
|
+
include ::Teckel::Operation
|
9
|
+
|
10
|
+
result!
|
11
|
+
|
12
|
+
input Types::Hash.schema(message: Types::String)
|
13
|
+
error none
|
14
|
+
output Types::String
|
15
|
+
|
16
|
+
def call(input)
|
17
|
+
success! input[:message].upcase
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Chain
|
22
|
+
include Teckel::Chain
|
23
|
+
|
24
|
+
step :message, Message
|
25
|
+
|
26
|
+
class Result < Teckel::Operation::Result
|
27
|
+
def initialize(value, success, step, opts = {})
|
28
|
+
super(value, success)
|
29
|
+
@step = step
|
30
|
+
@opts = opts
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
alias :[] :new # Alias the default constructor to :new
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_reader :opts, :step
|
38
|
+
end
|
39
|
+
|
40
|
+
result_constructor ->(value, success, step) {
|
41
|
+
result.new(value, success, step, time: Time.now.to_i)
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
RSpec.describe Teckel::Chain do
|
47
|
+
specify do
|
48
|
+
result = TeckelChainResultTest::Chain.call(message: "Hello World!")
|
49
|
+
expect(result).to be_successful
|
50
|
+
expect(result.success).to eq("HELLO WORLD!")
|
51
|
+
expect(result.opts).to include(time: kind_of(Integer))
|
52
|
+
end
|
53
|
+
end
|