linearly 0.1.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 +7 -0
- data/lib/linearly/errors/broken_contract.rb +80 -0
- data/lib/linearly/errors/state_not_returned.rb +29 -0
- data/lib/linearly/flow.rb +144 -0
- data/lib/linearly/mixins/flow_builder.rb +22 -0
- data/lib/linearly/mixins/reducer.rb +32 -0
- data/lib/linearly/runner.rb +38 -0
- data/lib/linearly/step/dynamic.rb +50 -0
- data/lib/linearly/step/static.rb +120 -0
- data/lib/linearly/validation.rb +190 -0
- data/lib/linearly/version.rb +3 -0
- data/lib/linearly.rb +12 -0
- data/spec/dynamic_step.rb +15 -0
- data/spec/errors/broken_contract_spec.rb +29 -0
- data/spec/errors/state_not_returned_spec.rb +13 -0
- data/spec/flow_spec.rb +75 -0
- data/spec/runner_spec.rb +47 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/static_step.rb +15 -0
- data/spec/step/dynamic_spec.rb +29 -0
- data/spec/step/static_spec.rb +52 -0
- data/spec/test_step.rb +16 -0
- data/spec/validation_spec.rb +107 -0
- metadata +327 -0
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module Linearly
|
4
|
+
# {Validation} provides a way to check inputs and outputs against a set of
|
5
|
+
# per-field expectations.
|
6
|
+
# @abstract
|
7
|
+
class Validation
|
8
|
+
# Constructor for a {Validation}
|
9
|
+
#
|
10
|
+
# @param expectations [Hash<Symbol, Expectation>] a hash of per-field
|
11
|
+
# expectations. An expectation can be +true+ (just checking for field
|
12
|
+
# presence), a class name (checking for value type) or a +Proc+
|
13
|
+
# taking a value and returning a +Boolean+.
|
14
|
+
#
|
15
|
+
# @api private
|
16
|
+
def initialize(expectations)
|
17
|
+
@expectations =
|
18
|
+
expectations
|
19
|
+
.map { |key, expectation| [key, Expectation.to_proc(expectation)] }
|
20
|
+
.to_h
|
21
|
+
end
|
22
|
+
|
23
|
+
# Call validation with a {State}
|
24
|
+
#
|
25
|
+
# @param state [Statefully::State]
|
26
|
+
#
|
27
|
+
# @return [Statefully::State]
|
28
|
+
# @api private
|
29
|
+
def call(state)
|
30
|
+
Validator
|
31
|
+
.new(expectations, state)
|
32
|
+
.validate(error_class)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Wrapped expectations
|
38
|
+
#
|
39
|
+
# @return [Hash<Symbol, Expectation>]
|
40
|
+
# @api private
|
41
|
+
attr_reader :expectations
|
42
|
+
|
43
|
+
# {Inputs} is a pre-flight {Validation} of {State} inputs
|
44
|
+
class Inputs < Validation
|
45
|
+
private
|
46
|
+
|
47
|
+
# Return associated error class
|
48
|
+
#
|
49
|
+
# @return [Class]
|
50
|
+
# @api private
|
51
|
+
def error_class
|
52
|
+
Errors::BrokenContract::Inputs
|
53
|
+
end
|
54
|
+
end # class Inputs
|
55
|
+
|
56
|
+
# {Inputs} is a post-flight {Validation} of {State} outputs
|
57
|
+
class Outputs < Validation
|
58
|
+
private
|
59
|
+
|
60
|
+
# Return associated error class
|
61
|
+
#
|
62
|
+
# @return [Class]
|
63
|
+
# @api private
|
64
|
+
def error_class
|
65
|
+
Errors::BrokenContract::Outputs
|
66
|
+
end
|
67
|
+
end # class Outputs
|
68
|
+
|
69
|
+
# {Validator} is a stateful helper applying expecations to a {State}
|
70
|
+
class Validator
|
71
|
+
# Constructor method for a {Validator}
|
72
|
+
#
|
73
|
+
# @param expectations [Hash<Symbol, Expectation>]
|
74
|
+
# @param state [Statefully::State]
|
75
|
+
#
|
76
|
+
# @api private
|
77
|
+
def initialize(expectations, state)
|
78
|
+
@expectations = expectations
|
79
|
+
@state = state
|
80
|
+
end
|
81
|
+
|
82
|
+
# Validate wrapped {State}, failing it with an error class if needed
|
83
|
+
#
|
84
|
+
# @param error_class [Class]
|
85
|
+
#
|
86
|
+
# @return [Statefully::State]
|
87
|
+
# @api private
|
88
|
+
def validate(error_class)
|
89
|
+
failures = invalid.merge(missing).freeze
|
90
|
+
return @state if failures.empty?
|
91
|
+
@state.fail(error_class.new(failures))
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
# Return the invalid fields
|
97
|
+
#
|
98
|
+
# @return [Hash<Field, Failure::Unexpected>]
|
99
|
+
# @api private
|
100
|
+
def invalid
|
101
|
+
@invalid ||= @expectations.map do |key, expectation|
|
102
|
+
next nil if missing.key?(key)
|
103
|
+
value = @state.fetch(key)
|
104
|
+
next nil if expectation.call(value)
|
105
|
+
[key, Failure::Unexpected.instance]
|
106
|
+
end.compact.to_h
|
107
|
+
end
|
108
|
+
|
109
|
+
# Return the missing fields
|
110
|
+
#
|
111
|
+
# @return [Hash<Field, Failure::Missing>]
|
112
|
+
# @api private
|
113
|
+
def missing
|
114
|
+
@missing ||=
|
115
|
+
@expectations
|
116
|
+
.keys
|
117
|
+
.reject { |key| @state.key?(key) }
|
118
|
+
.map { |key| [key, Failure::Missing.instance] }
|
119
|
+
.to_h
|
120
|
+
end
|
121
|
+
end # class Validator
|
122
|
+
private_constant :Validator
|
123
|
+
|
124
|
+
# {Failure} is a representation of a problem encountered when validating a
|
125
|
+
# single {State} field.
|
126
|
+
class Failure
|
127
|
+
include Singleton
|
128
|
+
|
129
|
+
# Human-readable representation of the {Failure}
|
130
|
+
#
|
131
|
+
# @return [String]
|
132
|
+
# @api public
|
133
|
+
# @example
|
134
|
+
# Linearly::Validation::Failure::Missing.instance.missing?
|
135
|
+
# => [missing]
|
136
|
+
def inspect
|
137
|
+
"[#{self.class.name.split('::').last.downcase}]"
|
138
|
+
end
|
139
|
+
|
140
|
+
# {Unexpected} is a type of {Failure} when a field does not exists
|
141
|
+
class Missing < Failure
|
142
|
+
# Check if the field is missing
|
143
|
+
#
|
144
|
+
# @return [FalseClass]
|
145
|
+
# @api public
|
146
|
+
# @example
|
147
|
+
# Linearly::Validation::Failure::Missing.instance.missing?
|
148
|
+
# => true
|
149
|
+
def missing?
|
150
|
+
true
|
151
|
+
end
|
152
|
+
end # class Missing
|
153
|
+
|
154
|
+
# {Unexpected} is a type of {Failure} when a field exists, but its value
|
155
|
+
# does not match expectations.
|
156
|
+
class Unexpected < Failure
|
157
|
+
# Check if the field is missing
|
158
|
+
#
|
159
|
+
# @return [TrueClass]
|
160
|
+
# @api public
|
161
|
+
# @example
|
162
|
+
# Linearly::Validation::Failure::Unexpected.instance.missing?
|
163
|
+
# => true
|
164
|
+
def missing?
|
165
|
+
false
|
166
|
+
end
|
167
|
+
end # class Unexpected
|
168
|
+
end # class Failure
|
169
|
+
|
170
|
+
# {Expectation} is a helper module to turn various types of expectations
|
171
|
+
# into {Proc}s.
|
172
|
+
module Expectation
|
173
|
+
# Turn one of the supported expecation types into a Proc
|
174
|
+
#
|
175
|
+
# This method reeks of :reek:TooManyStatements.
|
176
|
+
#
|
177
|
+
# @param expectation [Symbol|Class|Proc]
|
178
|
+
#
|
179
|
+
# @return [Proc]
|
180
|
+
# @api private
|
181
|
+
def to_proc(expectation)
|
182
|
+
klass = expectation.class
|
183
|
+
return ->(value) { value.is_a?(expectation) } if klass == Class
|
184
|
+
return ->(_) { true } if klass == TrueClass
|
185
|
+
expectation
|
186
|
+
end
|
187
|
+
module_function :to_proc
|
188
|
+
end # module Expectation
|
189
|
+
end # class Validation
|
190
|
+
end # module Linearly
|
data/lib/linearly.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'linearly/mixins/flow_builder'
|
2
|
+
require 'linearly/mixins/reducer'
|
3
|
+
require 'linearly/errors/broken_contract'
|
4
|
+
require 'linearly/errors/state_not_returned'
|
5
|
+
require 'linearly/flow'
|
6
|
+
require 'linearly/runner'
|
7
|
+
require 'linearly/step/dynamic'
|
8
|
+
require 'linearly/step/static'
|
9
|
+
require 'linearly/validation'
|
10
|
+
require 'linearly/version'
|
11
|
+
|
12
|
+
require 'statefully'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Linearly
|
4
|
+
module Errors
|
5
|
+
describe BrokenContract do
|
6
|
+
let(:failures) do
|
7
|
+
{
|
8
|
+
missing: Validation::Failure::Missing.instance,
|
9
|
+
unexpected: Validation::Failure::Unexpected.instance,
|
10
|
+
}
|
11
|
+
end
|
12
|
+
let(:error) { described_class.new(failures) }
|
13
|
+
|
14
|
+
describe BrokenContract::Inputs do
|
15
|
+
let(:message) { 'failed input expectations: [missing, unexpected]' }
|
16
|
+
|
17
|
+
it { expect(error.message).to eq message }
|
18
|
+
it { expect(error.failures).to eq failures }
|
19
|
+
end # describe BrokenContract::Inputs
|
20
|
+
|
21
|
+
describe BrokenContract::Outputs do
|
22
|
+
let(:message) { 'failed output expectations: [missing, unexpected]' }
|
23
|
+
|
24
|
+
it { expect(error.message).to eq message }
|
25
|
+
it { expect(error.failures).to eq failures }
|
26
|
+
end # describe BrokenContract::Outputs
|
27
|
+
end # describe BrokenContract
|
28
|
+
end # module Errors
|
29
|
+
end # module Linearly
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Linearly
|
4
|
+
module Errors
|
5
|
+
describe StateNotReturned do
|
6
|
+
let(:value) { 'surprise!' }
|
7
|
+
let(:error) { described_class.new(value) }
|
8
|
+
|
9
|
+
it { expect(error.message).to eq 'String is not a Statefully::State' }
|
10
|
+
it { expect(error.value).to eq value }
|
11
|
+
end # describe StateNotReturned
|
12
|
+
end # module Errors
|
13
|
+
end # module Linearly
|
data/spec/flow_spec.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Linearly
|
4
|
+
describe Flow do
|
5
|
+
let(:step1_proc) { ->(state) { state.succeed(new_key: :new_val) } }
|
6
|
+
let(:step1) do
|
7
|
+
TestStep.new(
|
8
|
+
{ key: String },
|
9
|
+
{ new_key: Symbol },
|
10
|
+
step1_proc,
|
11
|
+
)
|
12
|
+
end
|
13
|
+
let(:step2) do
|
14
|
+
TestStep.new(
|
15
|
+
{ other: Numeric },
|
16
|
+
{},
|
17
|
+
->(state) { state.succeed },
|
18
|
+
)
|
19
|
+
end
|
20
|
+
let(:flow) { described_class.new(step1, step2) }
|
21
|
+
|
22
|
+
describe '#inputs' do
|
23
|
+
let(:inputs) { flow.inputs }
|
24
|
+
|
25
|
+
it { expect(inputs.length).to eq 2 }
|
26
|
+
it { expect(inputs.fetch(:key)).to eq String }
|
27
|
+
it { expect(inputs.fetch(:other)).to eq Numeric }
|
28
|
+
end # describe '#inputs'
|
29
|
+
|
30
|
+
describe '#outputs' do
|
31
|
+
let(:outputs) { flow.outputs }
|
32
|
+
|
33
|
+
it { expect(outputs.length).to eq 1 }
|
34
|
+
it { expect(outputs.fetch(:new_key)).to eq Symbol }
|
35
|
+
end # describe '#outputs'
|
36
|
+
|
37
|
+
describe '#>>' do
|
38
|
+
it { expect(flow.>>(step1)).to be_a Flow }
|
39
|
+
end # describe '#>>'
|
40
|
+
|
41
|
+
describe '#call' do
|
42
|
+
let(:state) { Statefully::State.create(**args) }
|
43
|
+
let(:result) { flow.call(state) }
|
44
|
+
|
45
|
+
context 'with correct input' do
|
46
|
+
let(:args) { { key: 'val', other: 7 } }
|
47
|
+
|
48
|
+
it { expect(result).to be_successful }
|
49
|
+
it { expect(result.history.length).to eq 3 }
|
50
|
+
|
51
|
+
context 'with a throwing step' do
|
52
|
+
let(:step1_proc) { ->(_) { raise 'Boom!' } }
|
53
|
+
|
54
|
+
it { expect(result).not_to be_successful }
|
55
|
+
it { expect(result.error).to be_a RuntimeError }
|
56
|
+
end # context 'with a throwing step'
|
57
|
+
|
58
|
+
context 'with a step not returning State' do
|
59
|
+
let(:step1_proc) { ->(_) { 'surprise!' } }
|
60
|
+
|
61
|
+
it { expect(result).not_to be_successful }
|
62
|
+
it { expect(result.error).to be_a Errors::StateNotReturned }
|
63
|
+
end # context 'with a step not returning State'
|
64
|
+
end # context 'with correct input'
|
65
|
+
|
66
|
+
context 'with missing initial state' do
|
67
|
+
let(:args) { { key: 'val' } }
|
68
|
+
|
69
|
+
it { expect(result).to be_failed }
|
70
|
+
it { expect(result.error).to be_a Errors::BrokenContract::Inputs }
|
71
|
+
it { expect(result.history.length).to eq 2 }
|
72
|
+
end # context 'with missing initial state'
|
73
|
+
end # describe '#call'
|
74
|
+
end # describe Flow
|
75
|
+
end # module Linearly
|
data/spec/runner_spec.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Linearly
|
4
|
+
describe Runner do
|
5
|
+
let(:state) { Statefully::State.create(key: 'val') }
|
6
|
+
let(:behavior) { ->(state) { state.succeed(new_key: 'new_val') } }
|
7
|
+
let(:inputs) { { key: true } }
|
8
|
+
let(:outputs) { { new_key: true } }
|
9
|
+
let(:step) { TestStep.new(inputs, outputs, behavior) }
|
10
|
+
let(:result) { described_class.new(step).call(state) }
|
11
|
+
|
12
|
+
context 'when all goes well' do
|
13
|
+
it { expect(result).to be_successful }
|
14
|
+
it { expect(result).not_to be_finished }
|
15
|
+
it { expect(result.key).to eq 'val' }
|
16
|
+
end # context 'when all goes well'
|
17
|
+
|
18
|
+
shared_examples 'returns_error' do |error_class|
|
19
|
+
it { expect(result).not_to be_successful }
|
20
|
+
it { expect(result.error).to be_a error_class }
|
21
|
+
end # shared_examples 'returns_error'
|
22
|
+
|
23
|
+
context 'when input validation fails' do
|
24
|
+
let(:inputs) { { other: true } }
|
25
|
+
|
26
|
+
it_behaves_like 'returns_error', Errors::BrokenContract::Inputs
|
27
|
+
end # context 'when input validation fails'
|
28
|
+
|
29
|
+
context 'when step fails' do
|
30
|
+
let(:behavior) { ->(state) { state.fail(RuntimeError.new('Boom!')) } }
|
31
|
+
|
32
|
+
it_behaves_like 'returns_error', RuntimeError
|
33
|
+
end # context 'when step fails'
|
34
|
+
|
35
|
+
context 'when step finishes' do
|
36
|
+
let(:behavior) { ->(state) { state.finish } }
|
37
|
+
|
38
|
+
it { expect(result).to be_finished }
|
39
|
+
end # context 'when step finishes'
|
40
|
+
|
41
|
+
context 'when output validation fails' do
|
42
|
+
let(:outputs) { { other: true } }
|
43
|
+
|
44
|
+
it_behaves_like 'returns_error', Errors::BrokenContract::Outputs
|
45
|
+
end # context 'when output validation fails'
|
46
|
+
end # describe Runner
|
47
|
+
end # module Linearly
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
Bundler.setup
|
3
|
+
|
4
|
+
require 'pry'
|
5
|
+
|
6
|
+
if ENV['CI'] == 'true'
|
7
|
+
require 'simplecov'
|
8
|
+
SimpleCov.start { add_filter '/spec/' }
|
9
|
+
|
10
|
+
require 'codecov'
|
11
|
+
SimpleCov.formatter = SimpleCov::Formatter::Codecov
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'linearly'
|
15
|
+
|
16
|
+
require 'dynamic_step'
|
17
|
+
require 'static_step'
|
18
|
+
require 'test_step'
|
data/spec/static_step.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module Linearly
|
2
|
+
class StaticStep < Step::Static
|
3
|
+
def self.inputs
|
4
|
+
{ number: Integer }
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.outputs
|
8
|
+
{ string: String }
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
succeed(string: (number + 1).to_s)
|
13
|
+
end
|
14
|
+
end # class StaticStep
|
15
|
+
end # module Linearly
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Linearly
|
4
|
+
module Step
|
5
|
+
describe Dynamic do
|
6
|
+
let(:step) { DynamicStep.new }
|
7
|
+
|
8
|
+
describe '#inputs' do
|
9
|
+
it { expect { step.inputs }.to raise_error NotImplementedError }
|
10
|
+
end # describe '#inputs'
|
11
|
+
|
12
|
+
describe '#outputs' do
|
13
|
+
it { expect { step.outputs }.to raise_error NotImplementedError }
|
14
|
+
end # describe '#outputs'
|
15
|
+
|
16
|
+
describe '#call' do
|
17
|
+
let(:state) { Statefully::State.create }
|
18
|
+
|
19
|
+
it { expect { step.call(state) }.to raise_error NotImplementedError }
|
20
|
+
end # describe '#call'
|
21
|
+
|
22
|
+
describe '#>>' do
|
23
|
+
let(:step) { DynamicStep::Valid.new }
|
24
|
+
|
25
|
+
it { expect(step.>>(step)).to be_a Flow }
|
26
|
+
end # describe '#>>'
|
27
|
+
end # describe Dynamic
|
28
|
+
end # module Step
|
29
|
+
end # module Linearly
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Linearly
|
4
|
+
describe Step::Static do
|
5
|
+
let(:args) { {} }
|
6
|
+
let(:state) { Statefully::State.create(**args) }
|
7
|
+
|
8
|
+
it { expect { described_class.inputs }.to raise_error NotImplementedError }
|
9
|
+
it { expect { described_class.outputs }.to raise_error NotImplementedError }
|
10
|
+
|
11
|
+
describe '.call' do
|
12
|
+
let(:result) { described_class.call(state) }
|
13
|
+
|
14
|
+
it { expect { result }.to raise_error NotImplementedError }
|
15
|
+
end # describe '.call'
|
16
|
+
|
17
|
+
describe 'implementation' do
|
18
|
+
subject { StaticStep }
|
19
|
+
|
20
|
+
describe '.>>' do
|
21
|
+
let(:flow) { subject.>>(subject) }
|
22
|
+
|
23
|
+
it { expect(flow).to be_a Flow }
|
24
|
+
end # describe '.>>'
|
25
|
+
|
26
|
+
describe '.call' do
|
27
|
+
let(:result) { subject.call(state) }
|
28
|
+
|
29
|
+
context 'with missing input' do
|
30
|
+
let(:args) { {} }
|
31
|
+
|
32
|
+
it { expect(result).not_to be_successful }
|
33
|
+
it { expect(result.error).to be_a NoMethodError }
|
34
|
+
end # context 'with missing input'
|
35
|
+
|
36
|
+
context 'with correct input' do
|
37
|
+
let(:args) { { number: 7 } }
|
38
|
+
|
39
|
+
it { expect(result).to be_successful }
|
40
|
+
it { expect(result.string).to eq '8' }
|
41
|
+
end # context 'with correct input'
|
42
|
+
|
43
|
+
context 'with incorrect input' do
|
44
|
+
let(:args) { { number: '7' } }
|
45
|
+
|
46
|
+
it { expect(result).not_to be_successful }
|
47
|
+
it { expect(result.error).to be_a TypeError }
|
48
|
+
end # context 'with incorrect input'
|
49
|
+
end # describe '.call'
|
50
|
+
end # describe 'implementation'
|
51
|
+
end # describe Step::Static
|
52
|
+
end # module Linearly
|
data/spec/test_step.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Linearly
|
4
|
+
class TestStep
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
attr_reader :inputs, :outputs
|
8
|
+
def_delegators :@behavior, :call
|
9
|
+
|
10
|
+
def initialize(inputs, outputs, behavior)
|
11
|
+
@inputs = inputs
|
12
|
+
@outputs = outputs
|
13
|
+
@behavior = behavior
|
14
|
+
end
|
15
|
+
end # class TestStep
|
16
|
+
end # module Linearly
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Linearly
|
4
|
+
describe Validation do
|
5
|
+
let(:validation) { described_class.new(expectations).call(state) }
|
6
|
+
let(:state) { Statefully::State.create(key: 'val') }
|
7
|
+
|
8
|
+
shared_examples 'validation_fails' do |error_class|
|
9
|
+
it { expect(validation).to be_failed }
|
10
|
+
it { expect(validation.error).to be_a error_class }
|
11
|
+
end # shared_examples 'validation_fails'
|
12
|
+
|
13
|
+
shared_examples 'fails_when_missing_field' do |error_class|
|
14
|
+
let(:field) { :other }
|
15
|
+
|
16
|
+
it_behaves_like 'validation_fails', error_class
|
17
|
+
|
18
|
+
it { expect(validation.error).to be_a error_class }
|
19
|
+
|
20
|
+
context 'with failures' do
|
21
|
+
let(:failures) { validation.error.failures }
|
22
|
+
|
23
|
+
it { expect(validation.error.failures).to have_key(field) }
|
24
|
+
it { expect(validation.error.failures.fetch(field)).to be_missing }
|
25
|
+
end # context 'with failures'
|
26
|
+
end # shared_examples 'fails_when_missing_field'
|
27
|
+
|
28
|
+
shared_examples 'fails_with_unexpected_value' do |error_class|
|
29
|
+
it_behaves_like 'validation_fails', error_class
|
30
|
+
|
31
|
+
it { expect(validation.error).to be_a error_class }
|
32
|
+
|
33
|
+
context 'with failures' do
|
34
|
+
let(:failures) { validation.error.failures }
|
35
|
+
|
36
|
+
it { expect(validation.error.failures).to have_key(field) }
|
37
|
+
it { expect(validation.error.failures.fetch(field)).not_to be_missing }
|
38
|
+
end # context 'with failures'
|
39
|
+
end # shared_examples 'fails_with_unexpected_value'
|
40
|
+
|
41
|
+
shared_examples 'succeeds_and_returns_state' do
|
42
|
+
it { expect(validation).to be_successful }
|
43
|
+
it { expect(validation).to eq state }
|
44
|
+
end # shared_examples 'succeeds_and_returns_state'
|
45
|
+
|
46
|
+
shared_examples 'supports_presence_expectation' do |error_class|
|
47
|
+
let(:field) { :key }
|
48
|
+
let(:expectations) { { field => true } }
|
49
|
+
|
50
|
+
it_behaves_like 'fails_when_missing_field', error_class
|
51
|
+
it_behaves_like 'succeeds_and_returns_state'
|
52
|
+
end # shared_examples 'supports_presence_expectation'
|
53
|
+
|
54
|
+
shared_examples 'supports_class_expectation' do |error_class|
|
55
|
+
let(:field) { :key }
|
56
|
+
let(:klass) { String }
|
57
|
+
let(:expectations) { { field => klass } }
|
58
|
+
|
59
|
+
it_behaves_like 'fails_when_missing_field', error_class
|
60
|
+
|
61
|
+
context 'when expectation is met (default)' do
|
62
|
+
it_behaves_like 'succeeds_and_returns_state'
|
63
|
+
end # context 'when expectation is met (default)'
|
64
|
+
|
65
|
+
context 'when expectation is not met' do
|
66
|
+
let(:klass) { Numeric }
|
67
|
+
|
68
|
+
it_behaves_like 'fails_with_unexpected_value', error_class
|
69
|
+
end # context 'when expectation is not met'
|
70
|
+
end # shared_examples 'supports_class_expectation'
|
71
|
+
|
72
|
+
shared_examples 'supports_proc_expectation' do |error_class|
|
73
|
+
let(:field) { :key }
|
74
|
+
let(:klass) { String }
|
75
|
+
let(:expectation) { ->(val) { val.length == 3 } }
|
76
|
+
let(:expectations) { { field => expectation } }
|
77
|
+
|
78
|
+
it_behaves_like 'fails_when_missing_field', error_class
|
79
|
+
|
80
|
+
context 'when expectation is met (default)' do
|
81
|
+
it_behaves_like 'succeeds_and_returns_state'
|
82
|
+
end # context 'when expectation is met (default)'
|
83
|
+
|
84
|
+
context 'when expectation is not met' do
|
85
|
+
let(:expectation) { ->(val) { val.length == 4 } }
|
86
|
+
|
87
|
+
it_behaves_like 'fails_with_unexpected_value', error_class
|
88
|
+
end # context 'when expectation is not met'
|
89
|
+
end # shared_examples 'supports_proc_expectation'
|
90
|
+
|
91
|
+
describe Validation::Inputs do
|
92
|
+
error = Errors::BrokenContract::Inputs
|
93
|
+
|
94
|
+
it_behaves_like 'supports_presence_expectation', error
|
95
|
+
it_behaves_like 'supports_class_expectation', error
|
96
|
+
it_behaves_like 'supports_proc_expectation', error
|
97
|
+
end # describe Validation::Inputs
|
98
|
+
|
99
|
+
describe Validation::Outputs do
|
100
|
+
error = Errors::BrokenContract::Outputs
|
101
|
+
|
102
|
+
it_behaves_like 'supports_presence_expectation', error
|
103
|
+
it_behaves_like 'supports_class_expectation', error
|
104
|
+
it_behaves_like 'supports_proc_expectation', error
|
105
|
+
end # describe Validation::Outputs
|
106
|
+
end # describe Validation
|
107
|
+
end # module Linearly
|