contraction 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.
- 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: []
|