contracts 0.0.1
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.
- data/lib/builtin_contracts.rb +169 -0
- data/lib/contracts.rb +106 -0
- data/lib/decorators.rb +84 -0
- metadata +69 -0
@@ -0,0 +1,169 @@
|
|
1
|
+
module Contracts
|
2
|
+
class Num
|
3
|
+
def self.valid? val
|
4
|
+
val.is_a? Numeric
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class Pos
|
9
|
+
def self.valid? val
|
10
|
+
val > 0
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Neg
|
15
|
+
def self.valid? val
|
16
|
+
val < 0
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Any
|
21
|
+
def self.valid? val
|
22
|
+
true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class None
|
27
|
+
def self.valid? val
|
28
|
+
false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class CallableClass
|
33
|
+
def self.[](*vals)
|
34
|
+
self.new(*vals)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Or < CallableClass
|
39
|
+
def initialize(*vals)
|
40
|
+
@vals = vals
|
41
|
+
end
|
42
|
+
|
43
|
+
def valid?(val)
|
44
|
+
@vals.any? do |contract|
|
45
|
+
res, _ = Contract.valid?(val, contract)
|
46
|
+
res
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_s
|
51
|
+
@vals[0, @vals.size-1].join(", ") + " or " + @vals[-1].to_s
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Xor < CallableClass
|
56
|
+
def initialize(*vals)
|
57
|
+
@vals = vals
|
58
|
+
end
|
59
|
+
|
60
|
+
def valid?(val)
|
61
|
+
results = @vals.map do |contract|
|
62
|
+
res, _ = Contract.valid?(val, contract)
|
63
|
+
res
|
64
|
+
end
|
65
|
+
results.count(true) == 1
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_s
|
69
|
+
@vals[0, @vals.size-1].join(", ") + " xor " + @vals[-1].to_s
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class And < CallableClass
|
74
|
+
def initialize(*vals)
|
75
|
+
@vals = vals
|
76
|
+
end
|
77
|
+
|
78
|
+
def valid?(val)
|
79
|
+
@vals.all? do |contract|
|
80
|
+
res, _ = Contract.valid?(val, contract)
|
81
|
+
res
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_s
|
86
|
+
@vals[0, @vals.size-1].join(", ") + " and " + @vals[-1].to_s
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class RespondsTo < CallableClass
|
91
|
+
def initialize(*meths)
|
92
|
+
@meths = meths
|
93
|
+
end
|
94
|
+
|
95
|
+
def valid?(val)
|
96
|
+
@meths.all? do |meth|
|
97
|
+
val.respond_to? meth
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def to_s
|
102
|
+
"a value that responds to #{@meths.inspect}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class Send < CallableClass
|
107
|
+
def initialize(*meths)
|
108
|
+
@meths = meths
|
109
|
+
end
|
110
|
+
|
111
|
+
def valid?(val)
|
112
|
+
@meths.all? do |meth|
|
113
|
+
val.send(meth)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def to_s
|
118
|
+
"a value that returns true for all of #{@meths.inspect}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class IsA < CallableClass
|
123
|
+
def initialize(cls)
|
124
|
+
@cls = cls
|
125
|
+
end
|
126
|
+
|
127
|
+
def valid?(val)
|
128
|
+
val.is_a? @cls
|
129
|
+
end
|
130
|
+
|
131
|
+
def to_s
|
132
|
+
"a #{@cls.inspect}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class Not < CallableClass
|
137
|
+
def initialize(*vals)
|
138
|
+
@vals = vals
|
139
|
+
end
|
140
|
+
|
141
|
+
def valid?(val)
|
142
|
+
@vals.all? do |contract|
|
143
|
+
res, _ = Contract.valid?(val, contract)
|
144
|
+
!res
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def to_s
|
149
|
+
"a value that is none of #{@vals.inspect}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
class ArrayOf < CallableClass
|
154
|
+
def initialize(contract)
|
155
|
+
@contract = contract
|
156
|
+
end
|
157
|
+
|
158
|
+
def valid?(vals)
|
159
|
+
vals.all? do |val|
|
160
|
+
res, _ = Contract.valid?(val, @contract)
|
161
|
+
res
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def to_s
|
166
|
+
"an array of #{@contract}"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
data/lib/contracts.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'decorators'
|
2
|
+
require 'builtin_contracts'
|
3
|
+
|
4
|
+
class Class
|
5
|
+
include MethodDecorators
|
6
|
+
end
|
7
|
+
|
8
|
+
class Contract < Decorator
|
9
|
+
attr_accessor :contracts, :klass, :method
|
10
|
+
decorator_name :contract
|
11
|
+
def initialize(klass, method, *contracts)
|
12
|
+
@klass, @method, @contracts = klass, method, contracts
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.mkerror(validates, arg, contract)
|
16
|
+
if validates
|
17
|
+
[true, {}]
|
18
|
+
else
|
19
|
+
[false, { :arg => arg, :contract => contract }]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.failure_msg(data)
|
24
|
+
# TODO __file__ and __line__ won't work in Ruby 1.9.
|
25
|
+
# It provides a source_location method instead.
|
26
|
+
%{Contract violation:
|
27
|
+
Expected: #{data[:contract]},
|
28
|
+
Actual: #{data[:arg].inspect}
|
29
|
+
Value guarded in: #{data[:class]}::#{data[:method].name}
|
30
|
+
With Contract: #{data[:contracts].map { |t| t.is_a?(Class) ? t.name : t.class.name }.join(", ") }
|
31
|
+
At: #{data[:method].__file__}:#{data[:method].__line__} }
|
32
|
+
end
|
33
|
+
def self.failure_callback(data)
|
34
|
+
raise failure_msg(data)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.success_callback(data)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.validate_hash(arg, contract)
|
41
|
+
arg.keys.each do |k|
|
42
|
+
result, info = validate(arg[k], contract[k])
|
43
|
+
return [result, info] unless result
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.validate_proc(arg, contract)
|
48
|
+
mkerror(contract[arg], arg, contract)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.validate_class(arg, contract)
|
52
|
+
valid = if contract.respond_to? :valid?
|
53
|
+
contract.valid? arg
|
54
|
+
else
|
55
|
+
contract == arg.class
|
56
|
+
end
|
57
|
+
mkerror(valid, arg, contract)
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.validate_all(args, contracts, klass, method)
|
61
|
+
args.zip(contracts).each do |arg, contract|
|
62
|
+
validate(arg, contract, klass, method, contracts)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.validate(arg, contract, klass, method, contracts)
|
67
|
+
result, _ = valid?(arg, contract)
|
68
|
+
if result
|
69
|
+
success_callback({:arg => arg, :contract => contract, :class => klass, :method => method, :contracts => contracts})
|
70
|
+
else
|
71
|
+
failure_callback({:arg => arg, :contract => contract, :class => klass, :method => method, :contracts => contracts})
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# arg to method -> contract it should satisfy -> (Boolean, metadata)
|
76
|
+
def self.valid?(arg, contract)
|
77
|
+
case contract
|
78
|
+
when Class
|
79
|
+
validate_class arg, contract
|
80
|
+
when Proc
|
81
|
+
validate_proc arg, contract
|
82
|
+
when Array
|
83
|
+
# TODO account for these errors too
|
84
|
+
return mkerror(false, arg, contract) unless arg.is_a?(Array)
|
85
|
+
validate_all(arg, contract)
|
86
|
+
when Hash
|
87
|
+
return mkerror(false, arg, contract) unless arg.is_a?(Hash)
|
88
|
+
validate_hash(arg, contract)
|
89
|
+
else
|
90
|
+
if contract.respond_to? :valid?
|
91
|
+
mkerror(contract.valid?(arg), arg, contract)
|
92
|
+
else
|
93
|
+
mkerror(arg == contract, arg, contract)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def call(this, *args)
|
99
|
+
Contract.validate_all(args, @contracts, @klass, @method)
|
100
|
+
result = @method.bind(this).call(*args)
|
101
|
+
if args.size == @contracts.size - 1
|
102
|
+
Contract.validate(result, @contracts[-1], @klass, @method, @contracts)
|
103
|
+
end
|
104
|
+
result
|
105
|
+
end
|
106
|
+
end
|
data/lib/decorators.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
module MethodDecorators
|
2
|
+
def self.extended(klass)
|
3
|
+
class << klass
|
4
|
+
attr_accessor :decorated_methods
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
def method_missing(name, *args, &blk)
|
9
|
+
# if the const isn't capitalized, then const_defined will throw an
|
10
|
+
# error. So first check to make sure it is capitalized.
|
11
|
+
letter = name.to_s[0]
|
12
|
+
if letter >= 65 && letter <= 90 && Object.const_defined?(name)
|
13
|
+
const = Object.const_get(name)
|
14
|
+
elsif Decorator.decorators.key?(name)
|
15
|
+
const = Decorator.decorators[name]
|
16
|
+
else
|
17
|
+
return super
|
18
|
+
end
|
19
|
+
|
20
|
+
instance_eval <<-ruby_eval, __FILE__, __LINE__ + 1
|
21
|
+
def #{name}(*args, &blk)
|
22
|
+
decorate(#{const.name}, *args, &blk)
|
23
|
+
end
|
24
|
+
ruby_eval
|
25
|
+
|
26
|
+
send(name, *args, &blk)
|
27
|
+
end
|
28
|
+
|
29
|
+
def method_added(name)
|
30
|
+
return unless @decorators
|
31
|
+
|
32
|
+
decorators = @decorators.dup
|
33
|
+
@decorators = nil
|
34
|
+
@decorated_methods ||= Hash.new {|h,k| h[k] = []}
|
35
|
+
|
36
|
+
class << self; attr_accessor :decorated_methods; end
|
37
|
+
|
38
|
+
decorators.each do |klass, args|
|
39
|
+
decorator = klass.respond_to?(:new) ? klass.new(self, instance_method(name), *args) : klass
|
40
|
+
@decorated_methods[name] << decorator
|
41
|
+
end
|
42
|
+
|
43
|
+
class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
|
44
|
+
def #{name}(*args, &blk)
|
45
|
+
ret = nil
|
46
|
+
self.class.decorated_methods[#{name.inspect}].each do |decorator|
|
47
|
+
ret = decorator.call(self, *args, &blk)
|
48
|
+
end
|
49
|
+
ret
|
50
|
+
end
|
51
|
+
ruby_eval
|
52
|
+
end
|
53
|
+
|
54
|
+
def decorate(klass, *args)
|
55
|
+
@decorators ||= []
|
56
|
+
@decorators << [klass, args]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class Decorator
|
61
|
+
class << self
|
62
|
+
attr_accessor :decorators
|
63
|
+
def decorator_name(name)
|
64
|
+
Decorator.decorators ||= {}
|
65
|
+
Decorator.decorators[name] = self
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.inherited(klass)
|
70
|
+
name = klass.name.gsub(/^./) {|m| m.downcase}
|
71
|
+
|
72
|
+
return if name =~ /^[^A-Za-z_]/ || name =~ /[^0-9A-Za-z_]/
|
73
|
+
|
74
|
+
MethodDecorators.module_eval <<-ruby_eval, __FILE__, __LINE__ + 1
|
75
|
+
def #{klass}(*args, &blk)
|
76
|
+
decorate(#{klass}, *args, &blk)
|
77
|
+
end
|
78
|
+
ruby_eval
|
79
|
+
end
|
80
|
+
|
81
|
+
def initialize(klass, method)
|
82
|
+
@method = method
|
83
|
+
end
|
84
|
+
end
|
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: contracts
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Aditya Bhargava
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-06-07 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: "This library provides contracts for Ruby. Contracts let you clearly \xE2\x80\x93 even beautifully \xE2\x80\x93 express how your code behaves, and free you from writing tons of boilerplate, defensive code."
|
23
|
+
email: bluemangroupie@gmail.com
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files: []
|
29
|
+
|
30
|
+
files:
|
31
|
+
- lib/builtin_contracts.rb
|
32
|
+
- lib/contracts.rb
|
33
|
+
- lib/decorators.rb
|
34
|
+
has_rdoc: true
|
35
|
+
homepage: http://github.com/egonSchiele/contracts.ruby
|
36
|
+
licenses: []
|
37
|
+
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
hash: 3
|
49
|
+
segments:
|
50
|
+
- 0
|
51
|
+
version: "0"
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
rubyforge_project:
|
64
|
+
rubygems_version: 1.3.7
|
65
|
+
signing_key:
|
66
|
+
specification_version: 3
|
67
|
+
summary: Contracts for Ruby.
|
68
|
+
test_files: []
|
69
|
+
|