contraction 0.1.1 → 0.1.2

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 (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: