contraction 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/README.md +54 -0
  2. data/contraction.rb +2 -0
  3. data/lib/contraction.rb +92 -0
  4. data/lib/string.rb +13 -0
  5. 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
@@ -0,0 +1,2 @@
1
+ require 'lib/string'
2
+ require 'lib/contraction'
@@ -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: []