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