contraction 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +54 -0
- data/contraction.rb +2 -0
- data/lib/contraction.rb +92 -0
- data/lib/string.rb +13 -0
- metadata +53 -0
data/README.md
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
Contraction
|
2
|
+
===========
|
3
|
+
|
4
|
+
A code-by-contract library for ruby, using RDoc documentation to enforce the contract requirements
|
5
|
+
|
6
|
+
Basic usage
|
7
|
+
===========
|
8
|
+
|
9
|
+
Just include Contraction at the bottom of your class/module:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'contraction'
|
15
|
+
|
16
|
+
class MySuperCoolClass
|
17
|
+
##
|
18
|
+
# The foobar function takes a string and an integer between 1 and 100 and
|
19
|
+
# joins them, returning the result.
|
20
|
+
#
|
21
|
+
# @param [String] foo A string to which we add the number
|
22
|
+
# @param [Fixnum] bar A number to add to foo { bar >= 0 and bar <= 100 }
|
23
|
+
# @return [String] The concatonation of foo and bar { return.start_with?(foo) and return.include?(bar.to_s) }
|
24
|
+
def foobar(foo, bar)
|
25
|
+
"#{foo} #{bar.to_s}"
|
26
|
+
end
|
27
|
+
|
28
|
+
include Contraction
|
29
|
+
end
|
30
|
+
|
31
|
+
```
|
32
|
+
|
33
|
+
And you're done.
|
34
|
+
|
35
|
+
You define your normal documentation (you do document your code, don't you?)
|
36
|
+
using regular RDoc syntax. For the params and returns, however, you can add an
|
37
|
+
extra bit of information at the end. Any code put in between curly braces
|
38
|
+
(`{}`) is evaluated as the contract for that param or return. You just use the
|
39
|
+
names of the params for their values, and `return` for the return value. If you
|
40
|
+
provide a type for either the param or the return, type-checking will be
|
41
|
+
enforced as well. A full-qualified type is recommended (`Foo::Bar` instead of `Bar`.)
|
42
|
+
|
43
|
+
Warning
|
44
|
+
=======
|
45
|
+
|
46
|
+
This will slow things down. All-in-all you're looking at about an 8x increase
|
47
|
+
in overhead vs just calling a function. It is not recommended that you use this
|
48
|
+
for every method in a deeply-nested code-path. This overhead is more-or-less
|
49
|
+
in-line with other Ruby design-by-contract libraries, however, and with the
|
50
|
+
added benefit of free documentation.
|
51
|
+
|
52
|
+
Also, this is not super-heavily tested. I've been using it myself and thought I
|
53
|
+
would release it to the world, but I don't do a lot of RDoc-fu, so YMMV.
|
54
|
+
Pull-requests welcome.
|
data/contraction.rb
ADDED
data/lib/contraction.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'string'
|
2
|
+
module Contraction
|
3
|
+
def self.included(mod)
|
4
|
+
instance = mod.allocate
|
5
|
+
instance_methods = (mod.instance_methods - Object.instance_methods - Contraction.instance_methods)
|
6
|
+
|
7
|
+
file_contents_by_filename = {}
|
8
|
+
instance_methods.each do |method_name|
|
9
|
+
file, line = instance.method(method_name).source_location
|
10
|
+
filename = File.expand_path(file)
|
11
|
+
file_contents = file_contents_by_filename[filename] || File.read(filename).split("\n")
|
12
|
+
file_contents_by_filename ||= file_contents
|
13
|
+
|
14
|
+
args = []
|
15
|
+
returns = [Object, '', 'true']
|
16
|
+
file_contents[0..line-2].reverse.each do |line|
|
17
|
+
line = line.strip
|
18
|
+
next if line == ''
|
19
|
+
break unless line.start_with?('#')
|
20
|
+
break if line.start_with?('##')
|
21
|
+
|
22
|
+
# if m = /^#\s*@return\s+(?<type>\[[^\]]+\])?\s*(?<message>[^{]+)?(\{(?<contract>[^}]+)\})?/.match(line)
|
23
|
+
if m = /^#\s*@return\s+(\[[^\]]+\])?\s*([^{]+)?(\{([^}]+)\})?/.match(line)
|
24
|
+
type = m[1].to_s.gsub(/(\[|\])/, '')
|
25
|
+
type = type == '' ? Object : type.constantize
|
26
|
+
contract = m[4].to_s.strip.gsub('return', "result")
|
27
|
+
contract = contract == '' ? 'true' : contract
|
28
|
+
returns = [type, m[2], contract]
|
29
|
+
# elsif m = /^#\s*@param\s+(?<type>\[[^\]]+\])?\s*(?<name>[^\s]+)\s+(?<message>[^{]+)?(\{(?<contract>[^}]+)\})?/.match(line)
|
30
|
+
elsif m = /^#\s*@param\s+(\[[^\]]+\])?\s*([^\s]+)\s+([^{]+)?(\{([^}]+)\})?/.match(line)
|
31
|
+
type = m[1].to_s.gsub(/(\[|\])/, '')
|
32
|
+
type = type == '' ? Object : type.constantize
|
33
|
+
contract = m[5].to_s.strip.gsub(m[2], "named_args[#{m[2].inspect}]")
|
34
|
+
contract = contract == '' ? 'true' : contract
|
35
|
+
args << [type, m[2], m[3], contract]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
args.reverse!
|
39
|
+
arg_names = args.map { |a| a[1] }
|
40
|
+
arg_names.each do |name|
|
41
|
+
returns[-1] = returns[-1].gsub(name, "named_args[#{name.inspect}]")
|
42
|
+
end
|
43
|
+
|
44
|
+
old_method = mod.instance_method(method_name)
|
45
|
+
|
46
|
+
arg_checks = []
|
47
|
+
result_check = nil
|
48
|
+
mod.send(:define_method, method_name) do |*method_args|
|
49
|
+
named_args = args.each_with_index.inject({}) do |h, (arg, index)|
|
50
|
+
type, name, message, contract = arg
|
51
|
+
h[name] = method_args[index]
|
52
|
+
h
|
53
|
+
end
|
54
|
+
|
55
|
+
b = binding
|
56
|
+
if arg_checks.empty?
|
57
|
+
arg_checks = args.map do |arg|
|
58
|
+
type, name, message, contract = arg
|
59
|
+
value = named_args[name]
|
60
|
+
checker = nil
|
61
|
+
lambda { |named_args|
|
62
|
+
raise ArgumentError.new("#{name} (#{value.inspect}) must be a #{type}") unless value.is_a?(type)
|
63
|
+
checker = eval("lambda { |named_args| #{contract} }", b) if checker.nil?
|
64
|
+
unless checker.call(named_args)
|
65
|
+
raise ArgumentError.new("#{name} (#{message}) must fullfill #{contract.inspect}, but is #{value.inspect}")
|
66
|
+
end
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
arg_checks.each { |c| c.call(named_args) }
|
71
|
+
|
72
|
+
result = old_method.bind(self).call(*method_args)
|
73
|
+
|
74
|
+
if result_check.nil?
|
75
|
+
checker = nil
|
76
|
+
result_check = lambda { |named_args|
|
77
|
+
type, message, contract = returns
|
78
|
+
raise ArgumentError.new("Return value of #{method_name} must be a #{type}") unless result.is_a?(type)
|
79
|
+
checker = eval("lambda { |named_args| #{contract} }", b) if checker.nil?
|
80
|
+
unless checker.call(named_args)
|
81
|
+
raise ArgumentError.new("Return value of #{method_name} (#{message}) must fullfill #{contract.inspect}, but is #{result.inspect}")
|
82
|
+
end
|
83
|
+
}
|
84
|
+
end
|
85
|
+
result_check.call(named_args)
|
86
|
+
|
87
|
+
result
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
data/lib/string.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
class String
|
2
|
+
# This is taken strait copy-pasta from ActiveSupport. All praise be to them.
|
3
|
+
def constantize
|
4
|
+
names = self.split('::')
|
5
|
+
names.shift if names.empty? || names.first.empty?
|
6
|
+
|
7
|
+
constant = Object
|
8
|
+
names.each do |name|
|
9
|
+
constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
10
|
+
end
|
11
|
+
constant
|
12
|
+
end
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: contraction
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Thomas Luce
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-18 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Using RDoc documentation as your contract definition, you get solid code,
|
15
|
+
and good docs. Win-win!
|
16
|
+
email: thomas.luce@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files:
|
20
|
+
- README.md
|
21
|
+
files:
|
22
|
+
- README.md
|
23
|
+
- contraction.rb
|
24
|
+
- lib/contraction.rb
|
25
|
+
- lib/string.rb
|
26
|
+
homepage:
|
27
|
+
licenses: []
|
28
|
+
post_install_message:
|
29
|
+
rdoc_options: []
|
30
|
+
require_paths:
|
31
|
+
- lib
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
segments:
|
39
|
+
- 0
|
40
|
+
hash: 2451511498111927596
|
41
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
requirements: []
|
48
|
+
rubyforge_project:
|
49
|
+
rubygems_version: 1.8.24
|
50
|
+
signing_key:
|
51
|
+
specification_version: 3
|
52
|
+
summary: A simple desgin-by-contract library
|
53
|
+
test_files: []
|