contracts-lite 0.14.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/CHANGELOG.markdown +80 -0
- data/Gemfile +16 -0
- data/LICENSE +23 -0
- data/README.md +102 -0
- data/TODO.markdown +6 -0
- data/TUTORIAL.md +747 -0
- data/benchmarks/bench.rb +67 -0
- data/benchmarks/hash.rb +69 -0
- data/benchmarks/invariants.rb +91 -0
- data/benchmarks/io.rb +62 -0
- data/benchmarks/wrap_test.rb +57 -0
- data/contracts.gemspec +13 -0
- data/lib/contracts.rb +231 -0
- data/lib/contracts/builtin_contracts.rb +541 -0
- data/lib/contracts/call_with.rb +97 -0
- data/lib/contracts/core.rb +52 -0
- data/lib/contracts/decorators.rb +47 -0
- data/lib/contracts/engine.rb +26 -0
- data/lib/contracts/engine/base.rb +136 -0
- data/lib/contracts/engine/eigenclass.rb +50 -0
- data/lib/contracts/engine/target.rb +70 -0
- data/lib/contracts/error_formatter.rb +121 -0
- data/lib/contracts/errors.rb +71 -0
- data/lib/contracts/formatters.rb +134 -0
- data/lib/contracts/invariants.rb +68 -0
- data/lib/contracts/method_handler.rb +195 -0
- data/lib/contracts/method_reference.rb +100 -0
- data/lib/contracts/support.rb +59 -0
- data/lib/contracts/validators.rb +139 -0
- data/lib/contracts/version.rb +3 -0
- data/script/rubocop +7 -0
- data/spec/builtin_contracts_spec.rb +461 -0
- data/spec/contracts_spec.rb +748 -0
- data/spec/error_formatter_spec.rb +68 -0
- data/spec/fixtures/fixtures.rb +710 -0
- data/spec/invariants_spec.rb +17 -0
- data/spec/module_spec.rb +18 -0
- data/spec/override_validators_spec.rb +162 -0
- data/spec/ruby_version_specific/contracts_spec_1.9.rb +24 -0
- data/spec/ruby_version_specific/contracts_spec_2.0.rb +55 -0
- data/spec/ruby_version_specific/contracts_spec_2.1.rb +63 -0
- data/spec/spec_helper.rb +102 -0
- data/spec/support.rb +10 -0
- data/spec/support_spec.rb +21 -0
- data/spec/validators_spec.rb +47 -0
- metadata +94 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
module Contracts
|
2
|
+
RSpec.describe Invariants do
|
3
|
+
def new_subject
|
4
|
+
MyBirthday.new(31, 12)
|
5
|
+
end
|
6
|
+
|
7
|
+
it "works when all invariants are holding" do
|
8
|
+
expect { new_subject.clever_next_day! }.not_to raise_error
|
9
|
+
expect { new_subject.clever_next_month! }.not_to raise_error
|
10
|
+
end
|
11
|
+
|
12
|
+
it "raises invariant violation error when any of invariants are not holding" do
|
13
|
+
expect { new_subject.silly_next_day! }.to raise_error(InvariantError, /day condition to be true/)
|
14
|
+
expect { new_subject.silly_next_month! }.to raise_error(InvariantError, /month condition to be true/)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/spec/module_spec.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Mod
|
2
|
+
include Contracts::Core
|
3
|
+
|
4
|
+
Contract C::Num => C::Num
|
5
|
+
def self.a_module_method a
|
6
|
+
a + 1
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
RSpec.describe "module methods" do
|
11
|
+
it "should pass for correct input" do
|
12
|
+
expect { Mod.a_module_method(2) }.to_not raise_error
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should fail for incorrect input" do
|
16
|
+
expect { Mod.a_module_method("bad") }.to raise_error(ContractError)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
RSpec.describe Contract do
|
2
|
+
describe ".override_validator" do
|
3
|
+
around do |example|
|
4
|
+
Contract.reset_validators
|
5
|
+
example.run
|
6
|
+
Contract.reset_validators
|
7
|
+
end
|
8
|
+
|
9
|
+
it "allows to override simple validators" do
|
10
|
+
Contract.override_validator(Hash) do |contract|
|
11
|
+
lambda do |arg|
|
12
|
+
return false unless arg.is_a?(Hash)
|
13
|
+
# Any hash in my system should have :it_is_a_hash key!
|
14
|
+
return false unless arg.key?(:it_is_a_hash)
|
15
|
+
contract.keys.all? do |k|
|
16
|
+
Contract.valid?(arg[k], contract[k])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
klass = Class.new do
|
22
|
+
include Contracts::Core
|
23
|
+
|
24
|
+
Contract ({ :a => Contracts::Num, :b => String }) => nil
|
25
|
+
def something(opts)
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
obj = klass.new
|
31
|
+
|
32
|
+
expect do
|
33
|
+
obj.something(:a => 35, :b => "hello")
|
34
|
+
end.to raise_error(ContractError)
|
35
|
+
|
36
|
+
expect do
|
37
|
+
obj.something(
|
38
|
+
:a => 35,
|
39
|
+
:b => "hello",
|
40
|
+
:it_is_a_hash => true
|
41
|
+
)
|
42
|
+
end.not_to raise_error
|
43
|
+
end
|
44
|
+
|
45
|
+
it "allows to override valid contract" do
|
46
|
+
Contract.override_validator(:valid) do |contract|
|
47
|
+
if contract.respond_to?(:in_valid_state?)
|
48
|
+
lambda do |arg|
|
49
|
+
contract.in_valid_state? && contract.valid?(arg)
|
50
|
+
end
|
51
|
+
else
|
52
|
+
lambda { |arg| contract.valid?(arg) }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
stateful_contract = Class.new(Contracts::CallableClass) do
|
57
|
+
def initialize(contract)
|
58
|
+
@contract = contract
|
59
|
+
@state = 0
|
60
|
+
end
|
61
|
+
|
62
|
+
def in_valid_state?
|
63
|
+
@state < 3
|
64
|
+
end
|
65
|
+
|
66
|
+
def valid?(arg)
|
67
|
+
@state += 1
|
68
|
+
Contract.valid?(arg, @contract)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
klass = Class.new do
|
73
|
+
include Contracts::Core
|
74
|
+
|
75
|
+
Contract stateful_contract[Contracts::Num] => Contracts::Num
|
76
|
+
def only_three_times(x)
|
77
|
+
x * x
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
obj = klass.new
|
82
|
+
|
83
|
+
expect(obj.only_three_times(3)).to eq(9)
|
84
|
+
expect(obj.only_three_times(3)).to eq(9)
|
85
|
+
expect(obj.only_three_times(3)).to eq(9)
|
86
|
+
|
87
|
+
expect do
|
88
|
+
obj.only_three_times(3)
|
89
|
+
end.to raise_error(ContractError)
|
90
|
+
|
91
|
+
expect do
|
92
|
+
obj.only_three_times(3)
|
93
|
+
end.to raise_error(ContractError)
|
94
|
+
end
|
95
|
+
|
96
|
+
it "allows to override class validator" do
|
97
|
+
# Make contracts accept all rspec doubles
|
98
|
+
Contract.override_validator(:class) do |contract|
|
99
|
+
lambda do |arg|
|
100
|
+
arg.is_a?(RSpec::Mocks::Double) ||
|
101
|
+
arg.is_a?(contract)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
klass = Class.new do
|
106
|
+
include Contracts::Core
|
107
|
+
|
108
|
+
Contract String => String
|
109
|
+
def greet(name)
|
110
|
+
"hello, #{name}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
obj = klass.new
|
115
|
+
|
116
|
+
expect(obj.greet("world")).to eq("hello, world")
|
117
|
+
|
118
|
+
expect do
|
119
|
+
obj.greet(4)
|
120
|
+
end.to raise_error(ContractError)
|
121
|
+
|
122
|
+
expect(obj.greet(double("name"))).to match(
|
123
|
+
/hello, #\[.*Double.*"name"\]/
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
it "allows to override default validator" do
|
128
|
+
spy = double("spy")
|
129
|
+
|
130
|
+
Contract.override_validator(:default) do |contract|
|
131
|
+
lambda do |arg|
|
132
|
+
spy.log("#{arg} == #{contract}")
|
133
|
+
arg == contract
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
klass = Class.new do
|
138
|
+
include Contracts::Core
|
139
|
+
|
140
|
+
Contract 1, Contracts::Num => Contracts::Num
|
141
|
+
def gcd(_, b)
|
142
|
+
b
|
143
|
+
end
|
144
|
+
|
145
|
+
Contract Contracts::Num, Contracts::Num => Contracts::Num
|
146
|
+
def gcd(a, b)
|
147
|
+
gcd(b % a, a)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
obj = klass.new
|
152
|
+
|
153
|
+
expect(spy).to receive(:log).with("8 == 1").ordered.once
|
154
|
+
expect(spy).to receive(:log).with("5 == 1").ordered.once
|
155
|
+
expect(spy).to receive(:log).with("3 == 1").ordered.once
|
156
|
+
expect(spy).to receive(:log).with("2 == 1").ordered.once
|
157
|
+
expect(spy).to receive(:log).with("1 == 1").ordered.once
|
158
|
+
|
159
|
+
obj.gcd(8, 5)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class GenericExample
|
2
|
+
Contract C::Args[String], C::Num => C::ArrayOf[String]
|
3
|
+
def splat_then_arg(*vals, n)
|
4
|
+
vals.map { |v| v * n }
|
5
|
+
end
|
6
|
+
|
7
|
+
if ruby_version <= 1.9
|
8
|
+
Contract ({:foo => C::Nat}) => nil
|
9
|
+
def nat_test_with_kwarg(a_hash)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
RSpec.describe "Contracts:" do
|
15
|
+
before :all do
|
16
|
+
@o = GenericExample.new
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "Splat not last (or penultimate to block)" do
|
20
|
+
it "should work with arg after splat" do
|
21
|
+
expect { @o.splat_then_arg("hello", "world", 3) }.to_not raise_error
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
class GenericExample
|
2
|
+
Contract C::Args[String], { repeat: C::Maybe[C::Num] } => C::ArrayOf[String]
|
3
|
+
def splat_then_optional_named(*vals, repeat: 2)
|
4
|
+
vals.map { |v| v * repeat }
|
5
|
+
end
|
6
|
+
|
7
|
+
Contract ({foo: C::Nat}) => nil
|
8
|
+
def nat_test_with_kwarg(foo: 10)
|
9
|
+
end
|
10
|
+
|
11
|
+
Contract C::KeywordArgs[name: C::Optional[String]], C::Func[String => String] => String
|
12
|
+
def keyword_args_hello(name: "Adit", &block)
|
13
|
+
"Hey, #{yield name}!"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
RSpec.describe "Contracts:" do
|
18
|
+
before :all do
|
19
|
+
@o = GenericExample.new
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "Optional named arguments" do
|
23
|
+
it "should work with optional named argument unfilled after splat" do
|
24
|
+
expect { @o.splat_then_optional_named("hello", "world") }.to_not raise_error
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should work with optional named argument filled after splat" do
|
28
|
+
expect { @o.splat_then_optional_named("hello", "world", repeat: 3) }.to_not raise_error
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "Nat:" do
|
33
|
+
it "should pass for keyword args with correct arg given" do
|
34
|
+
expect { @o.nat_test_with_kwarg(foo: 10) }.to_not raise_error
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should fail with a ContractError for wrong keyword args input" do
|
38
|
+
expect { @o.nat_test_with_kwarg(foo: -10) }.to raise_error(ContractError)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should fail with a ContractError for no input" do
|
42
|
+
expect { @o.nat_test_with_kwarg }.to raise_error(ContractError)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "keyword args with defaults, with a block" do
|
47
|
+
it "should work when both keyword args and a block is given" do
|
48
|
+
expect(@o.keyword_args_hello(name: "maggie", &:upcase)).to eq("Hey, MAGGIE!")
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should work even when keyword args aren't given" do
|
52
|
+
expect(@o.keyword_args_hello(&:upcase)).to eq("Hey, ADIT!")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
class GenericExample
|
2
|
+
Contract String, C::Bool, C::Args[Symbol], Float, C::KeywordArgs[e: Range, f: C::Optional[C::Num], g: Symbol], Proc =>
|
3
|
+
[Proc, Hash, C::Maybe[C::Num], Range, Float, C::ArrayOf[Symbol], C::Bool, String]
|
4
|
+
def complicated(a, b = true, *c, d, e:, f:2, **g, &h)
|
5
|
+
h.call [h, g, f, e, d, c, b, a]
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
RSpec.describe "Contracts:" do
|
10
|
+
before :all do
|
11
|
+
@o = GenericExample.new
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "Required named arguments" do
|
15
|
+
describe "really complicated method signature" do
|
16
|
+
it "should work with default named args used" do
|
17
|
+
expect do
|
18
|
+
@o.complicated("a", false, :b, 2.0, e: (1..5), g: :d) { |x| x }
|
19
|
+
end.to_not raise_error
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should work with all args filled manually, with extra splat and hash" do
|
23
|
+
expect do
|
24
|
+
@o.complicated("a", true, :b, :c, 2.0, e: (1..5), f: 8.3, g: :d) do |x|
|
25
|
+
x
|
26
|
+
end
|
27
|
+
end.to_not raise_error
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should fail when the return is invalid" do
|
31
|
+
expect do
|
32
|
+
@o.complicated("a", true, :b, 2.0, e: (1..5)) { |_x| "bad" }
|
33
|
+
end.to raise_error(ContractError)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should fail when args are invalid" do
|
37
|
+
expect do
|
38
|
+
@o.complicated("a", "bad", :b, 2.0, e: (1..5)) { |x| x }
|
39
|
+
end.to raise_error(ContractError)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should fail when splat is invalid" do
|
43
|
+
expect do
|
44
|
+
@o.complicated("a", true, "bad", 2.0, e: (1..5)) { |x| x }
|
45
|
+
end.to raise_error(ContractError)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should fail when named argument is invalid" do
|
49
|
+
expect do
|
50
|
+
@o.complicated("a", true, :b, 2.0, e: "bad") { |x| x }
|
51
|
+
end.to raise_error(ContractError)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should fail when passed nil to an optional argument which contract shouldnt accept nil" do
|
55
|
+
expect do
|
56
|
+
@o.complicated("a", true, :b, :c, 2.0, e: (1..5), f: nil, g: :d) do |x|
|
57
|
+
x
|
58
|
+
end
|
59
|
+
end.to raise_error(ContractError, /Expected: \(KeywordArgs\[{:e=>Range, :f=>Optional\[Num\], :g=>Symbol}\]\)/)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require "contracts"
|
2
|
+
require File.expand_path(File.join(__FILE__, "../support"))
|
3
|
+
require File.expand_path(File.join(__FILE__, "../fixtures/fixtures"))
|
4
|
+
|
5
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
6
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
7
|
+
# The generated `.rspec` file contains `--require spec_helper` which will cause this
|
8
|
+
# file to always be loaded, without a need to explicitly require it in any files.
|
9
|
+
#
|
10
|
+
# Given that it is always loaded, you are encouraged to keep this file as
|
11
|
+
# light-weight as possible. Requiring heavyweight dependencies from this file
|
12
|
+
# will add to the boot time of your test suite on EVERY test run, even for an
|
13
|
+
# individual file that may not need all of that loaded. Instead, consider making
|
14
|
+
# a separate helper file that requires the additional dependencies and performs
|
15
|
+
# the additional setup, and require it from the spec files that actually need it.
|
16
|
+
#
|
17
|
+
# The `.rspec` file also contains a few flags that are not defaults but that
|
18
|
+
# users commonly want.
|
19
|
+
#
|
20
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
21
|
+
RSpec.configure do |config|
|
22
|
+
config.pattern = "*.rb"
|
23
|
+
|
24
|
+
# Only load tests who's syntax is valid in the current Ruby
|
25
|
+
[1.9, 2.0, 2.1].each do |ver|
|
26
|
+
config.pattern << ",ruby_version_specific/*#{ver}.rb" if ruby_version >= ver
|
27
|
+
end
|
28
|
+
|
29
|
+
# rspec-expectations config goes here. You can use an alternate
|
30
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
31
|
+
# assertions if you prefer.
|
32
|
+
config.expect_with :rspec do |expectations|
|
33
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
34
|
+
# and `failure_message` of custom matchers include text for helper methods
|
35
|
+
# defined using `chain`, e.g.:
|
36
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
37
|
+
# # => "be bigger than 2 and smaller than 4"
|
38
|
+
# ...rather than:
|
39
|
+
# # => "be bigger than 2"
|
40
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
41
|
+
end
|
42
|
+
|
43
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
44
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
45
|
+
config.mock_with :rspec do |mocks|
|
46
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
47
|
+
# a real object. This is generally recommended, and will default to
|
48
|
+
# `true` in RSpec 4.
|
49
|
+
mocks.verify_partial_doubles = true
|
50
|
+
end
|
51
|
+
|
52
|
+
# These two settings work together to allow you to limit a spec run
|
53
|
+
# to individual examples or groups you care about by tagging them with
|
54
|
+
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
55
|
+
# get run.
|
56
|
+
config.filter_run :focus
|
57
|
+
config.run_all_when_everything_filtered = true
|
58
|
+
|
59
|
+
# Limits the available syntax to the non-monkey patched syntax that is recommended.
|
60
|
+
# For more details, see:
|
61
|
+
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
62
|
+
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
63
|
+
# - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
|
64
|
+
config.disable_monkey_patching!
|
65
|
+
|
66
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
67
|
+
# be too noisy due to issues in dependencies.
|
68
|
+
# config.warnings = true
|
69
|
+
|
70
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
71
|
+
# file, and it's useful to allow more verbose output when running an
|
72
|
+
# individual spec file.
|
73
|
+
if config.files_to_run.one?
|
74
|
+
# Use the documentation formatter for detailed output,
|
75
|
+
# unless a formatter has already been configured
|
76
|
+
# (e.g. via a command-line flag).
|
77
|
+
config.default_formatter = "doc"
|
78
|
+
end
|
79
|
+
|
80
|
+
# Print the 10 slowest examples and example groups at the
|
81
|
+
# end of the spec run, to help surface which specs are running
|
82
|
+
# particularly slow.
|
83
|
+
config.profile_examples = 10
|
84
|
+
|
85
|
+
# Run specs in random order to surface order dependencies. If you find an
|
86
|
+
# order dependency and want to debug it, you can fix the order by providing
|
87
|
+
# the seed, which is printed after each run.
|
88
|
+
# --seed 1234
|
89
|
+
# Unable to use it now
|
90
|
+
config.order = :random
|
91
|
+
|
92
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
93
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
94
|
+
# test failures related to randomization by passing the same `--seed` value
|
95
|
+
# as the one that triggered the failure.
|
96
|
+
Kernel.srand config.seed
|
97
|
+
|
98
|
+
# Callbacks
|
99
|
+
config.after :each do
|
100
|
+
::Contract.restore_failure_callback
|
101
|
+
end
|
102
|
+
end
|