teckel 0.1.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|