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,97 @@
|
|
1
|
+
module Contracts
|
2
|
+
module CallWith
|
3
|
+
def call_with(this, *args, &blk)
|
4
|
+
args << blk if blk
|
5
|
+
|
6
|
+
# Explicitly append blk=nil if nil != Proc contract violation anticipated
|
7
|
+
nil_block_appended = maybe_append_block!(args, blk)
|
8
|
+
|
9
|
+
# Explicitly append options={} if Hash contract is present
|
10
|
+
maybe_append_options!(args, blk)
|
11
|
+
|
12
|
+
# Loop forward validating the arguments up to the splat (if there is one)
|
13
|
+
(@args_contract_index || args.size).times do |i|
|
14
|
+
contract = args_contracts[i]
|
15
|
+
arg = args[i]
|
16
|
+
validator = @args_validators[i]
|
17
|
+
|
18
|
+
unless validator && validator[arg]
|
19
|
+
return unless Contract.failure_callback(:arg => arg,
|
20
|
+
:contract => contract,
|
21
|
+
:class => klass,
|
22
|
+
:method => method,
|
23
|
+
:contracts => self,
|
24
|
+
:arg_pos => i+1,
|
25
|
+
:total_args => args.size,
|
26
|
+
:return_value => false)
|
27
|
+
end
|
28
|
+
|
29
|
+
if contract.is_a?(Contracts::Func)
|
30
|
+
args[i] = Contract.new(klass, arg, *contract.contracts)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# If there is a splat loop backwards to the lower index of the splat
|
35
|
+
# Once we hit the splat in this direction set its upper index
|
36
|
+
# Keep validating but use this upper index to get the splat validator.
|
37
|
+
if @args_contract_index
|
38
|
+
splat_upper_index = @args_contract_index
|
39
|
+
(args.size - @args_contract_index).times do |i|
|
40
|
+
arg = args[args.size - 1 - i]
|
41
|
+
|
42
|
+
if args_contracts[args_contracts.size - 1 - i].is_a?(Contracts::Args)
|
43
|
+
splat_upper_index = i
|
44
|
+
end
|
45
|
+
|
46
|
+
# Each arg after the spat is found must use the splat validator
|
47
|
+
j = i < splat_upper_index ? i : splat_upper_index
|
48
|
+
contract = args_contracts[args_contracts.size - 1 - j]
|
49
|
+
validator = @args_validators[args_contracts.size - 1 - j]
|
50
|
+
|
51
|
+
unless validator && validator[arg]
|
52
|
+
return unless Contract.failure_callback(:arg => arg,
|
53
|
+
:contract => contract,
|
54
|
+
:class => klass,
|
55
|
+
:method => method,
|
56
|
+
:contracts => self,
|
57
|
+
:arg_pos => i-1,
|
58
|
+
:total_args => args.size,
|
59
|
+
:return_value => false)
|
60
|
+
end
|
61
|
+
|
62
|
+
if contract.is_a?(Contracts::Func)
|
63
|
+
args[args.size - 1 - i] = Contract.new(klass, arg, *contract.contracts)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# If we put the block into args for validating, restore the args
|
69
|
+
# OR if we added a fake nil at the end because a block wasn't passed in.
|
70
|
+
args.slice!(-1) if blk || nil_block_appended
|
71
|
+
result = if method.respond_to?(:call)
|
72
|
+
# proc, block, lambda, etc
|
73
|
+
method.call(*args, &blk)
|
74
|
+
else
|
75
|
+
# original method name referrence
|
76
|
+
method.send_to(this, *args, &blk)
|
77
|
+
end
|
78
|
+
|
79
|
+
unless @ret_validator[result]
|
80
|
+
Contract.failure_callback(:arg => result,
|
81
|
+
:contract => ret_contract,
|
82
|
+
:class => klass,
|
83
|
+
:method => method,
|
84
|
+
:contracts => self,
|
85
|
+
:return_value => true)
|
86
|
+
end
|
87
|
+
|
88
|
+
this.verify_invariants!(method) if this.respond_to?(:verify_invariants!)
|
89
|
+
|
90
|
+
if ret_contract.is_a?(Contracts::Func)
|
91
|
+
result = Contract.new(klass, result, *ret_contract.contracts)
|
92
|
+
end
|
93
|
+
|
94
|
+
result
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Contracts
|
2
|
+
module Core
|
3
|
+
def self.included(base)
|
4
|
+
common(base)
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.extended(base)
|
8
|
+
common(base)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.common(base)
|
12
|
+
base.extend(MethodDecorators)
|
13
|
+
|
14
|
+
base.instance_eval do
|
15
|
+
def functype(funcname)
|
16
|
+
contracts = Engine.fetch_from(self).decorated_methods_for(:class_methods, funcname)
|
17
|
+
if contracts.nil?
|
18
|
+
"No contract for #{self}.#{funcname}"
|
19
|
+
else
|
20
|
+
"#{funcname} :: #{contracts[0]}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# NOTE: Workaround for `defined?(super)` bug in ruby 1.9.2
|
26
|
+
# source: http://stackoverflow.com/a/11181685
|
27
|
+
# bug: https://bugs.ruby-lang.org/issues/6644
|
28
|
+
base.class_eval <<-RUBY
|
29
|
+
# TODO: deprecate
|
30
|
+
# Required when contracts are included in global scope
|
31
|
+
def Contract(*args)
|
32
|
+
if defined?(super)
|
33
|
+
super
|
34
|
+
else
|
35
|
+
self.class.Contract(*args)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
RUBY
|
39
|
+
|
40
|
+
base.class_eval do
|
41
|
+
def functype(funcname)
|
42
|
+
contracts = Engine.fetch_from(self.class).decorated_methods_for(:instance_methods, funcname)
|
43
|
+
if contracts.nil?
|
44
|
+
"No contract for #{self.class}.#{funcname}"
|
45
|
+
else
|
46
|
+
"#{funcname} :: #{contracts[0]}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Contracts
|
2
|
+
module MethodDecorators
|
3
|
+
def self.extended(klass)
|
4
|
+
Engine.apply(klass)
|
5
|
+
end
|
6
|
+
|
7
|
+
def inherited(subclass)
|
8
|
+
Engine.fetch_from(subclass).set_eigenclass_owner
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def method_added(name)
|
13
|
+
MethodHandler.new(name, false, self).handle
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def singleton_method_added(name)
|
18
|
+
MethodHandler.new(name, true, self).handle
|
19
|
+
super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Decorator
|
24
|
+
# an attr_accessor for a class variable:
|
25
|
+
class << self; attr_accessor :decorators; end
|
26
|
+
|
27
|
+
def self.inherited(klass)
|
28
|
+
name = klass.name.gsub(/^./) { |m| m.downcase }
|
29
|
+
|
30
|
+
return if name =~ /^[^A-Za-z_]/ || name =~ /[^0-9A-Za-z_]/
|
31
|
+
|
32
|
+
# the file and line parameters set the text for error messages
|
33
|
+
# make a new method that is the name of your decorator.
|
34
|
+
# that method accepts random args and a block.
|
35
|
+
# inside, `decorate` is called with those params.
|
36
|
+
MethodDecorators.module_eval <<-ruby_eval, __FILE__, __LINE__ + 1
|
37
|
+
def #{klass}(*args, &blk)
|
38
|
+
::Contracts::Engine.fetch_from(self).decorate(#{klass}, *args, &blk)
|
39
|
+
end
|
40
|
+
ruby_eval
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(klass, method)
|
44
|
+
@method = method
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "contracts/engine/base"
|
2
|
+
require "contracts/engine/target"
|
3
|
+
require "contracts/engine/eigenclass"
|
4
|
+
|
5
|
+
require "forwardable"
|
6
|
+
|
7
|
+
module Contracts
|
8
|
+
# Engine facade, normally you shouldn't refer internals of Engine
|
9
|
+
# module directly.
|
10
|
+
module Engine
|
11
|
+
class << self
|
12
|
+
extend Forwardable
|
13
|
+
|
14
|
+
# .apply(klass) - enables contracts engine on klass
|
15
|
+
# .applied?(klass) - returns true if klass has contracts engine
|
16
|
+
# .fetch_from(klass) - returns contracts engine for klass
|
17
|
+
def_delegators :base_engine, :apply, :applied?, :fetch_from
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def base_engine
|
22
|
+
Base
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module Contracts
|
2
|
+
module Engine
|
3
|
+
# Contracts engine
|
4
|
+
class Base
|
5
|
+
# Enable contracts engine for klass
|
6
|
+
#
|
7
|
+
# @param [Class] klass - target class
|
8
|
+
def self.apply(klass)
|
9
|
+
Engine::Target.new(klass).apply
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns true if klass has contracts engine
|
13
|
+
#
|
14
|
+
# @param [Class] klass - target class
|
15
|
+
# @return [Bool]
|
16
|
+
def self.applied?(klass)
|
17
|
+
Engine::Target.new(klass).applied?
|
18
|
+
end
|
19
|
+
|
20
|
+
# Fetches contracts engine out of klass
|
21
|
+
#
|
22
|
+
# @param [Class] klass - target class
|
23
|
+
# @return [Engine::Base or Engine::Eigenclass]
|
24
|
+
def self.fetch_from(klass)
|
25
|
+
Engine::Target.new(klass).engine
|
26
|
+
end
|
27
|
+
|
28
|
+
# Creates new instance of contracts engine
|
29
|
+
#
|
30
|
+
# @param [Class] klass - class that owns this engine
|
31
|
+
def initialize(klass)
|
32
|
+
@klass = klass
|
33
|
+
end
|
34
|
+
|
35
|
+
# Adds provided decorator to the engine
|
36
|
+
# It validates that decorator can be added to this engine at the
|
37
|
+
# moment
|
38
|
+
#
|
39
|
+
# @param [Decorator:Class] decorator_class
|
40
|
+
# @param args - arguments for decorator
|
41
|
+
def decorate(decorator_class, *args)
|
42
|
+
validate!
|
43
|
+
decorators << [decorator_class, args]
|
44
|
+
end
|
45
|
+
|
46
|
+
# Sets eigenclass' owner to klass
|
47
|
+
def set_eigenclass_owner
|
48
|
+
eigenclass_engine.owner_class = klass
|
49
|
+
end
|
50
|
+
|
51
|
+
# Fetches all accumulated decorators (both this engine and
|
52
|
+
# corresponding eigenclass' engine)
|
53
|
+
# It clears all accumulated decorators
|
54
|
+
#
|
55
|
+
# @return [ArrayOf[Decorator]]
|
56
|
+
def all_decorators
|
57
|
+
pop_decorators + eigenclass_engine.all_decorators
|
58
|
+
end
|
59
|
+
|
60
|
+
# Fetches decorators of specified type for method with name
|
61
|
+
#
|
62
|
+
# @param [Or[:class_methods, :instance_methods]] type - method type
|
63
|
+
# @param [Symbol] name - method name
|
64
|
+
# @return [ArrayOf[Decorator]]
|
65
|
+
def decorated_methods_for(type, name)
|
66
|
+
Array(decorated_methods[type][name])
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns true if there are any decorated methods
|
70
|
+
#
|
71
|
+
# @return [Bool]
|
72
|
+
def decorated_methods?
|
73
|
+
!decorated_methods[:class_methods].empty? ||
|
74
|
+
!decorated_methods[:instance_methods].empty?
|
75
|
+
end
|
76
|
+
|
77
|
+
# Adds method decorator
|
78
|
+
#
|
79
|
+
# @param [Or[:class_methods, :instance_methods]] type - method type
|
80
|
+
# @param [Symbol] name - method name
|
81
|
+
# @param [Decorator] decorator - method decorator
|
82
|
+
def add_method_decorator(type, name, decorator)
|
83
|
+
decorated_methods[type][name] ||= []
|
84
|
+
decorated_methods[type][name] << decorator
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns nearest ancestor's engine that has decorated methods
|
88
|
+
#
|
89
|
+
# @return [Engine::Base or Engine::Eigenclass]
|
90
|
+
def nearest_decorated_ancestor
|
91
|
+
current = klass
|
92
|
+
current_engine = self
|
93
|
+
ancestors = current.ancestors[1..-1]
|
94
|
+
|
95
|
+
while current && current_engine && !current_engine.decorated_methods?
|
96
|
+
current = ancestors.shift
|
97
|
+
current_engine = Engine.fetch_from(current)
|
98
|
+
end
|
99
|
+
|
100
|
+
current_engine
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
attr_reader :klass
|
106
|
+
|
107
|
+
def decorated_methods
|
108
|
+
@_decorated_methods ||= { :class_methods => {}, :instance_methods => {} }
|
109
|
+
end
|
110
|
+
|
111
|
+
# No-op because it is safe to add decorators to normal classes
|
112
|
+
def validate!
|
113
|
+
end
|
114
|
+
|
115
|
+
def pop_decorators
|
116
|
+
decorators.tap { clear_decorators }
|
117
|
+
end
|
118
|
+
|
119
|
+
def eigenclass
|
120
|
+
Support.eigenclass_of(klass)
|
121
|
+
end
|
122
|
+
|
123
|
+
def eigenclass_engine
|
124
|
+
Eigenclass.lift(eigenclass, klass)
|
125
|
+
end
|
126
|
+
|
127
|
+
def decorators
|
128
|
+
@_decorators ||= []
|
129
|
+
end
|
130
|
+
|
131
|
+
def clear_decorators
|
132
|
+
@_decorators = []
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Contracts
|
2
|
+
module Engine
|
3
|
+
# Special case of contracts engine for eigenclasses
|
4
|
+
# We don't care about eigenclass of eigenclass at this point
|
5
|
+
class Eigenclass < Base
|
6
|
+
# Class that owns this eigenclass
|
7
|
+
attr_accessor :owner_class
|
8
|
+
|
9
|
+
# Automatically enables eigenclass engine if it is not
|
10
|
+
# Returns its engine
|
11
|
+
# NOTE: Required by jruby in 1.9 mode. Otherwise inherited
|
12
|
+
# eigenclasses don't have their engines
|
13
|
+
#
|
14
|
+
# @param [Class] eigenclass - class in question
|
15
|
+
# @param [Class] owner - owner of eigenclass
|
16
|
+
# @return [Engine::Eigenclass]
|
17
|
+
def self.lift(eigenclass, owner)
|
18
|
+
return Engine.fetch_from(eigenclass) if Engine.applied?(eigenclass)
|
19
|
+
|
20
|
+
Target.new(eigenclass).apply(Eigenclass)
|
21
|
+
eigenclass.extend(MethodDecorators)
|
22
|
+
# FIXME; this should detect what user uses `include Contracts` or
|
23
|
+
# `include Contracts;;Core`
|
24
|
+
eigenclass.send(:include, Contracts)
|
25
|
+
Engine.fetch_from(owner).set_eigenclass_owner
|
26
|
+
Engine.fetch_from(eigenclass)
|
27
|
+
end
|
28
|
+
|
29
|
+
# No-op for eigenclasses
|
30
|
+
def set_eigenclass_owner
|
31
|
+
end
|
32
|
+
|
33
|
+
# Fetches just eigenclasses decorators
|
34
|
+
def all_decorators
|
35
|
+
pop_decorators
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# Fails when contracts are not included in owner class
|
41
|
+
def validate!
|
42
|
+
fail ContractsNotIncluded unless owner?
|
43
|
+
end
|
44
|
+
|
45
|
+
def owner?
|
46
|
+
!!owner_class
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Contracts
|
2
|
+
module Engine
|
3
|
+
# Represents class in question
|
4
|
+
class Target
|
5
|
+
# Creates new instance of Target
|
6
|
+
#
|
7
|
+
# @param [Class] target - class in question
|
8
|
+
def initialize(target)
|
9
|
+
@target = target
|
10
|
+
end
|
11
|
+
|
12
|
+
# Enable contracts engine for target
|
13
|
+
# - it is no-op if contracts engine is already enabled
|
14
|
+
# - it automatically enables contracts engine for its eigenclass
|
15
|
+
# - it sets owner class to target for its eigenclass
|
16
|
+
#
|
17
|
+
# @param [Engine::Base:Class] engine_class - type of engine to
|
18
|
+
# enable (Base or Eigenclass)
|
19
|
+
def apply(engine_class = Base)
|
20
|
+
return if applied?
|
21
|
+
|
22
|
+
apply_to_eigenclass
|
23
|
+
|
24
|
+
eigenclass.class_eval do
|
25
|
+
define_method(:__contracts_engine) do
|
26
|
+
@__contracts_engine ||= engine_class.new(self)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
engine.set_eigenclass_owner
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns true if target has contracts engine already
|
34
|
+
#
|
35
|
+
# @return [Bool]
|
36
|
+
def applied?
|
37
|
+
target.respond_to?(:__contracts_engine)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns contracts engine of target
|
41
|
+
#
|
42
|
+
# @return [Engine::Base or Engine::Eigenclass]
|
43
|
+
def engine
|
44
|
+
applied? && target.__contracts_engine
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
attr_reader :target
|
50
|
+
|
51
|
+
def apply_to_eigenclass
|
52
|
+
return unless meaningless_eigenclass?
|
53
|
+
|
54
|
+
self.class.new(eigenclass).apply(Eigenclass)
|
55
|
+
eigenclass.extend(MethodDecorators)
|
56
|
+
# FIXME; this should detect what user uses `include Contracts` or
|
57
|
+
# `include Contracts;;Core`
|
58
|
+
eigenclass.send(:include, Contracts)
|
59
|
+
end
|
60
|
+
|
61
|
+
def eigenclass
|
62
|
+
Support.eigenclass_of(target)
|
63
|
+
end
|
64
|
+
|
65
|
+
def meaningless_eigenclass?
|
66
|
+
!Support.eigenclass?(target)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|