contraction 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/lib/contract.rb +39 -0
  2. data/lib/contraction.rb +90 -80
  3. metadata +4 -3
data/lib/contract.rb ADDED
@@ -0,0 +1,39 @@
1
+ module Contraction
2
+ private
3
+
4
+ class Contract
5
+ attr_accessor :type, :name, :message, :contract
6
+ def initialize(args={})
7
+ @type = args[:type]
8
+ @name = args[:name]
9
+ @message = args[:message]
10
+ @contract = args[:contract] || ''
11
+
12
+ create_checkers!
13
+ end
14
+
15
+ def check!(value, named_args)
16
+ @type_checker.call(value, named_args) if @type_checker
17
+ if @contract_checker
18
+ raise ArgumentError.new(contract_message(value)) unless @contract_checker.call(value, named_args)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def create_checkers!
25
+ if type
26
+ @type_checker = lambda { |value, named_args| raise ArgumentError.new(type_message(value)) unless value.is_a?(type) }
27
+ end
28
+ @contract_checker = eval("lambda { |result, named_args| #{contract} }") unless contract == ''
29
+ end
30
+
31
+ def type_message(value)
32
+ "#{name} (#{value.inspect}) must be a #{type}"
33
+ end
34
+
35
+ def contract_message(value)
36
+ "#{name} (#{message}) must fullfill #{contract.inspect}, but is #{value.inspect}"
37
+ end
38
+ end
39
+ end
data/lib/contraction.rb CHANGED
@@ -1,92 +1,102 @@
1
1
  require 'string'
2
+ require 'contract'
2
3
  module Contraction
3
- def self.included(mod)
4
+ def self.patch_instance_method(mod, method_name)
4
5
  instance = mod.allocate
5
- instance_methods = (mod.instance_methods - Object.instance_methods - Contraction.instance_methods)
6
+ args, returns = parse_comments(instance.method(method_name).source_location)
6
7
 
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
8
+ arg_names = args.map(&:name)
9
+ arg_names.each do |name|
10
+ returns.contract = returns.contract.gsub(name, "named_args[#{name.inspect}]")
11
+ end
12
+
13
+ old_method = mod.instance_method(method_name)
14
+
15
+ mod.send(:define_method, method_name) do |*method_args|
16
+ named_args = args.each_with_index.inject({}) do |h, (arg, index)|
17
+ h[arg.name] = method_args[index]
18
+ h
37
19
  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}]")
20
+
21
+ args.each { |arg| arg.check!(named_args[arg.name], named_args) }
22
+ result = old_method.bind(self).call(*method_args)
23
+ returns.check!(result, named_args)
24
+
25
+ result
26
+ end
27
+ end
28
+
29
+ def self.patch_class_method(mod, method_name)
30
+ args, returns = parse_comments(mod.method(method_name).source_location)
31
+ arg_names = args.map(&:name)
32
+ arg_names.each do |name|
33
+ returns.contract = returns.contract.gsub(name, "named_args[#{name.inspect}]")
34
+ end
35
+
36
+ old_method = mod.method(method_name)
37
+
38
+ arg_checks = []
39
+ result_check = nil
40
+ mod.define_singleton_method(method_name) do |*method_args|
41
+ named_args = args.each_with_index.inject({}) do |h, (arg, index)|
42
+ h[arg.name] = method_args[index]
43
+ h
42
44
  end
43
45
 
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
46
+ args.each { |arg| arg.check!(named_args[arg.name], named_args) }
47
+ result = old_method.call(*method_args)
48
+ returns.check!(result, named_args)
49
+
50
+ result
51
+ end
52
+ end
53
+
54
+ def self.file_content(filename)
55
+ @file_contents ||= {}
56
+ @file_contents[filename] ||= File.read(filename)
57
+ end
58
+
59
+ def self.parse_comments(location)
60
+ file, line = location
61
+ filename = File.expand_path(file)
62
+
63
+ args = []
64
+ returns = Contraction::Contract.new()
65
+ file_content(filename).split("\n")[0..line-2].reverse.each do |line|
66
+ line = line.strip
67
+ next if line == ''
68
+ break unless line.start_with?('#')
69
+ break if line.start_with?('##')
70
+
71
+ if m = /^#\s*@return\s+(\[[^\]]+\])?\s*([^{]+)?(\{([^}]+)\})?/.match(line)
72
+ type = m[1].to_s.gsub(/(\[|\])/, '')
73
+ type = type == '' ? Object : type.constantize
74
+ contract = m[4].to_s.strip.gsub('return', "result")
75
+ contract = contract == '' ? 'true' : contract
76
+ returns = Contraction::Contract.new(type: type, name: 'returns', message: m[2], contract: contract)
77
+ elsif m = /^#\s*@param\s+(\[[^\]]+\])?\s*([^\s]+)\s+([^{]+)?(\{([^}]+)\})?/.match(line)
78
+ type = m[1].to_s.gsub(/(\[|\])/, '')
79
+ type = type == '' ? Object : type.constantize
80
+ contract = m[5].to_s.strip.gsub(m[2], "named_args[#{m[2].inspect}]")
81
+ contract = contract == '' ? 'true' : contract
82
+ args << Contraction::Contract.new(type: type, name: m[2], message: m[3], contract: contract)
88
83
  end
89
84
  end
85
+ args.reverse!
86
+ return [args, returns]
87
+ end
88
+
89
+ def self.included(mod)
90
+ instance_methods = (mod.instance_methods - Object.instance_methods - Contraction.instance_methods)
91
+
92
+ instance_methods.each do |method_name|
93
+ patch_instance_method(mod, method_name)
94
+ end
95
+
96
+ class_methods = (mod.methods - Object.methods - Contraction.methods)
97
+ class_methods.each do |method_name|
98
+ patch_class_method(mod, method_name)
99
+ end
90
100
  end
91
101
  end
92
102
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: contraction
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-04-18 00:00:00.000000000 Z
12
+ date: 2013-04-19 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: Using RDoc documentation as your contract definition, you get solid code,
15
15
  and good docs. Win-win!
@@ -21,6 +21,7 @@ extra_rdoc_files:
21
21
  files:
22
22
  - README.md
23
23
  - contraction.rb
24
+ - lib/contract.rb
24
25
  - lib/contraction.rb
25
26
  - lib/string.rb
26
27
  homepage: https://github.com/thomasluce/contraction
@@ -37,7 +38,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
37
38
  version: '0'
38
39
  segments:
39
40
  - 0
40
- hash: 3590968264039948476
41
+ hash: 2360464299481397474
41
42
  required_rubygems_version: !ruby/object:Gem::Requirement
42
43
  none: false
43
44
  requirements: