contracts-lite 0.14.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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.markdown +80 -0
  3. data/Gemfile +16 -0
  4. data/LICENSE +23 -0
  5. data/README.md +102 -0
  6. data/TODO.markdown +6 -0
  7. data/TUTORIAL.md +747 -0
  8. data/benchmarks/bench.rb +67 -0
  9. data/benchmarks/hash.rb +69 -0
  10. data/benchmarks/invariants.rb +91 -0
  11. data/benchmarks/io.rb +62 -0
  12. data/benchmarks/wrap_test.rb +57 -0
  13. data/contracts.gemspec +13 -0
  14. data/lib/contracts.rb +231 -0
  15. data/lib/contracts/builtin_contracts.rb +541 -0
  16. data/lib/contracts/call_with.rb +97 -0
  17. data/lib/contracts/core.rb +52 -0
  18. data/lib/contracts/decorators.rb +47 -0
  19. data/lib/contracts/engine.rb +26 -0
  20. data/lib/contracts/engine/base.rb +136 -0
  21. data/lib/contracts/engine/eigenclass.rb +50 -0
  22. data/lib/contracts/engine/target.rb +70 -0
  23. data/lib/contracts/error_formatter.rb +121 -0
  24. data/lib/contracts/errors.rb +71 -0
  25. data/lib/contracts/formatters.rb +134 -0
  26. data/lib/contracts/invariants.rb +68 -0
  27. data/lib/contracts/method_handler.rb +195 -0
  28. data/lib/contracts/method_reference.rb +100 -0
  29. data/lib/contracts/support.rb +59 -0
  30. data/lib/contracts/validators.rb +139 -0
  31. data/lib/contracts/version.rb +3 -0
  32. data/script/rubocop +7 -0
  33. data/spec/builtin_contracts_spec.rb +461 -0
  34. data/spec/contracts_spec.rb +748 -0
  35. data/spec/error_formatter_spec.rb +68 -0
  36. data/spec/fixtures/fixtures.rb +710 -0
  37. data/spec/invariants_spec.rb +17 -0
  38. data/spec/module_spec.rb +18 -0
  39. data/spec/override_validators_spec.rb +162 -0
  40. data/spec/ruby_version_specific/contracts_spec_1.9.rb +24 -0
  41. data/spec/ruby_version_specific/contracts_spec_2.0.rb +55 -0
  42. data/spec/ruby_version_specific/contracts_spec_2.1.rb +63 -0
  43. data/spec/spec_helper.rb +102 -0
  44. data/spec/support.rb +10 -0
  45. data/spec/support_spec.rb +21 -0
  46. data/spec/validators_spec.rb +47 -0
  47. metadata +94 -0
@@ -0,0 +1,97 @@
1
+ module Contracts
2
+ module CallWith
3
+ def call_with(this, *args, &blk)
4
+ args << blk if blk
5
+
6
+ # Explicitly append blk=nil if nil != Proc contract violation anticipated
7
+ nil_block_appended = maybe_append_block!(args, blk)
8
+
9
+ # Explicitly append options={} if Hash contract is present
10
+ maybe_append_options!(args, blk)
11
+
12
+ # Loop forward validating the arguments up to the splat (if there is one)
13
+ (@args_contract_index || args.size).times do |i|
14
+ contract = args_contracts[i]
15
+ arg = args[i]
16
+ validator = @args_validators[i]
17
+
18
+ unless validator && validator[arg]
19
+ return unless Contract.failure_callback(:arg => arg,
20
+ :contract => contract,
21
+ :class => klass,
22
+ :method => method,
23
+ :contracts => self,
24
+ :arg_pos => i+1,
25
+ :total_args => args.size,
26
+ :return_value => false)
27
+ end
28
+
29
+ if contract.is_a?(Contracts::Func)
30
+ args[i] = Contract.new(klass, arg, *contract.contracts)
31
+ end
32
+ end
33
+
34
+ # If there is a splat loop backwards to the lower index of the splat
35
+ # Once we hit the splat in this direction set its upper index
36
+ # Keep validating but use this upper index to get the splat validator.
37
+ if @args_contract_index
38
+ splat_upper_index = @args_contract_index
39
+ (args.size - @args_contract_index).times do |i|
40
+ arg = args[args.size - 1 - i]
41
+
42
+ if args_contracts[args_contracts.size - 1 - i].is_a?(Contracts::Args)
43
+ splat_upper_index = i
44
+ end
45
+
46
+ # Each arg after the spat is found must use the splat validator
47
+ j = i < splat_upper_index ? i : splat_upper_index
48
+ contract = args_contracts[args_contracts.size - 1 - j]
49
+ validator = @args_validators[args_contracts.size - 1 - j]
50
+
51
+ unless validator && validator[arg]
52
+ return unless Contract.failure_callback(:arg => arg,
53
+ :contract => contract,
54
+ :class => klass,
55
+ :method => method,
56
+ :contracts => self,
57
+ :arg_pos => i-1,
58
+ :total_args => args.size,
59
+ :return_value => false)
60
+ end
61
+
62
+ if contract.is_a?(Contracts::Func)
63
+ args[args.size - 1 - i] = Contract.new(klass, arg, *contract.contracts)
64
+ end
65
+ end
66
+ end
67
+
68
+ # If we put the block into args for validating, restore the args
69
+ # OR if we added a fake nil at the end because a block wasn't passed in.
70
+ args.slice!(-1) if blk || nil_block_appended
71
+ result = if method.respond_to?(:call)
72
+ # proc, block, lambda, etc
73
+ method.call(*args, &blk)
74
+ else
75
+ # original method name referrence
76
+ method.send_to(this, *args, &blk)
77
+ end
78
+
79
+ unless @ret_validator[result]
80
+ Contract.failure_callback(:arg => result,
81
+ :contract => ret_contract,
82
+ :class => klass,
83
+ :method => method,
84
+ :contracts => self,
85
+ :return_value => true)
86
+ end
87
+
88
+ this.verify_invariants!(method) if this.respond_to?(:verify_invariants!)
89
+
90
+ if ret_contract.is_a?(Contracts::Func)
91
+ result = Contract.new(klass, result, *ret_contract.contracts)
92
+ end
93
+
94
+ result
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,52 @@
1
+ module Contracts
2
+ module Core
3
+ def self.included(base)
4
+ common(base)
5
+ end
6
+
7
+ def self.extended(base)
8
+ common(base)
9
+ end
10
+
11
+ def self.common(base)
12
+ base.extend(MethodDecorators)
13
+
14
+ base.instance_eval do
15
+ def functype(funcname)
16
+ contracts = Engine.fetch_from(self).decorated_methods_for(:class_methods, funcname)
17
+ if contracts.nil?
18
+ "No contract for #{self}.#{funcname}"
19
+ else
20
+ "#{funcname} :: #{contracts[0]}"
21
+ end
22
+ end
23
+ end
24
+
25
+ # NOTE: Workaround for `defined?(super)` bug in ruby 1.9.2
26
+ # source: http://stackoverflow.com/a/11181685
27
+ # bug: https://bugs.ruby-lang.org/issues/6644
28
+ base.class_eval <<-RUBY
29
+ # TODO: deprecate
30
+ # Required when contracts are included in global scope
31
+ def Contract(*args)
32
+ if defined?(super)
33
+ super
34
+ else
35
+ self.class.Contract(*args)
36
+ end
37
+ end
38
+ RUBY
39
+
40
+ base.class_eval do
41
+ def functype(funcname)
42
+ contracts = Engine.fetch_from(self.class).decorated_methods_for(:instance_methods, funcname)
43
+ if contracts.nil?
44
+ "No contract for #{self.class}.#{funcname}"
45
+ else
46
+ "#{funcname} :: #{contracts[0]}"
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,47 @@
1
+ module Contracts
2
+ module MethodDecorators
3
+ def self.extended(klass)
4
+ Engine.apply(klass)
5
+ end
6
+
7
+ def inherited(subclass)
8
+ Engine.fetch_from(subclass).set_eigenclass_owner
9
+ super
10
+ end
11
+
12
+ def method_added(name)
13
+ MethodHandler.new(name, false, self).handle
14
+ super
15
+ end
16
+
17
+ def singleton_method_added(name)
18
+ MethodHandler.new(name, true, self).handle
19
+ super
20
+ end
21
+ end
22
+
23
+ class Decorator
24
+ # an attr_accessor for a class variable:
25
+ class << self; attr_accessor :decorators; end
26
+
27
+ def self.inherited(klass)
28
+ name = klass.name.gsub(/^./) { |m| m.downcase }
29
+
30
+ return if name =~ /^[^A-Za-z_]/ || name =~ /[^0-9A-Za-z_]/
31
+
32
+ # the file and line parameters set the text for error messages
33
+ # make a new method that is the name of your decorator.
34
+ # that method accepts random args and a block.
35
+ # inside, `decorate` is called with those params.
36
+ MethodDecorators.module_eval <<-ruby_eval, __FILE__, __LINE__ + 1
37
+ def #{klass}(*args, &blk)
38
+ ::Contracts::Engine.fetch_from(self).decorate(#{klass}, *args, &blk)
39
+ end
40
+ ruby_eval
41
+ end
42
+
43
+ def initialize(klass, method)
44
+ @method = method
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,26 @@
1
+ require "contracts/engine/base"
2
+ require "contracts/engine/target"
3
+ require "contracts/engine/eigenclass"
4
+
5
+ require "forwardable"
6
+
7
+ module Contracts
8
+ # Engine facade, normally you shouldn't refer internals of Engine
9
+ # module directly.
10
+ module Engine
11
+ class << self
12
+ extend Forwardable
13
+
14
+ # .apply(klass) - enables contracts engine on klass
15
+ # .applied?(klass) - returns true if klass has contracts engine
16
+ # .fetch_from(klass) - returns contracts engine for klass
17
+ def_delegators :base_engine, :apply, :applied?, :fetch_from
18
+
19
+ private
20
+
21
+ def base_engine
22
+ Base
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,136 @@
1
+ module Contracts
2
+ module Engine
3
+ # Contracts engine
4
+ class Base
5
+ # Enable contracts engine for klass
6
+ #
7
+ # @param [Class] klass - target class
8
+ def self.apply(klass)
9
+ Engine::Target.new(klass).apply
10
+ end
11
+
12
+ # Returns true if klass has contracts engine
13
+ #
14
+ # @param [Class] klass - target class
15
+ # @return [Bool]
16
+ def self.applied?(klass)
17
+ Engine::Target.new(klass).applied?
18
+ end
19
+
20
+ # Fetches contracts engine out of klass
21
+ #
22
+ # @param [Class] klass - target class
23
+ # @return [Engine::Base or Engine::Eigenclass]
24
+ def self.fetch_from(klass)
25
+ Engine::Target.new(klass).engine
26
+ end
27
+
28
+ # Creates new instance of contracts engine
29
+ #
30
+ # @param [Class] klass - class that owns this engine
31
+ def initialize(klass)
32
+ @klass = klass
33
+ end
34
+
35
+ # Adds provided decorator to the engine
36
+ # It validates that decorator can be added to this engine at the
37
+ # moment
38
+ #
39
+ # @param [Decorator:Class] decorator_class
40
+ # @param args - arguments for decorator
41
+ def decorate(decorator_class, *args)
42
+ validate!
43
+ decorators << [decorator_class, args]
44
+ end
45
+
46
+ # Sets eigenclass' owner to klass
47
+ def set_eigenclass_owner
48
+ eigenclass_engine.owner_class = klass
49
+ end
50
+
51
+ # Fetches all accumulated decorators (both this engine and
52
+ # corresponding eigenclass' engine)
53
+ # It clears all accumulated decorators
54
+ #
55
+ # @return [ArrayOf[Decorator]]
56
+ def all_decorators
57
+ pop_decorators + eigenclass_engine.all_decorators
58
+ end
59
+
60
+ # Fetches decorators of specified type for method with name
61
+ #
62
+ # @param [Or[:class_methods, :instance_methods]] type - method type
63
+ # @param [Symbol] name - method name
64
+ # @return [ArrayOf[Decorator]]
65
+ def decorated_methods_for(type, name)
66
+ Array(decorated_methods[type][name])
67
+ end
68
+
69
+ # Returns true if there are any decorated methods
70
+ #
71
+ # @return [Bool]
72
+ def decorated_methods?
73
+ !decorated_methods[:class_methods].empty? ||
74
+ !decorated_methods[:instance_methods].empty?
75
+ end
76
+
77
+ # Adds method decorator
78
+ #
79
+ # @param [Or[:class_methods, :instance_methods]] type - method type
80
+ # @param [Symbol] name - method name
81
+ # @param [Decorator] decorator - method decorator
82
+ def add_method_decorator(type, name, decorator)
83
+ decorated_methods[type][name] ||= []
84
+ decorated_methods[type][name] << decorator
85
+ end
86
+
87
+ # Returns nearest ancestor's engine that has decorated methods
88
+ #
89
+ # @return [Engine::Base or Engine::Eigenclass]
90
+ def nearest_decorated_ancestor
91
+ current = klass
92
+ current_engine = self
93
+ ancestors = current.ancestors[1..-1]
94
+
95
+ while current && current_engine && !current_engine.decorated_methods?
96
+ current = ancestors.shift
97
+ current_engine = Engine.fetch_from(current)
98
+ end
99
+
100
+ current_engine
101
+ end
102
+
103
+ private
104
+
105
+ attr_reader :klass
106
+
107
+ def decorated_methods
108
+ @_decorated_methods ||= { :class_methods => {}, :instance_methods => {} }
109
+ end
110
+
111
+ # No-op because it is safe to add decorators to normal classes
112
+ def validate!
113
+ end
114
+
115
+ def pop_decorators
116
+ decorators.tap { clear_decorators }
117
+ end
118
+
119
+ def eigenclass
120
+ Support.eigenclass_of(klass)
121
+ end
122
+
123
+ def eigenclass_engine
124
+ Eigenclass.lift(eigenclass, klass)
125
+ end
126
+
127
+ def decorators
128
+ @_decorators ||= []
129
+ end
130
+
131
+ def clear_decorators
132
+ @_decorators = []
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,50 @@
1
+ module Contracts
2
+ module Engine
3
+ # Special case of contracts engine for eigenclasses
4
+ # We don't care about eigenclass of eigenclass at this point
5
+ class Eigenclass < Base
6
+ # Class that owns this eigenclass
7
+ attr_accessor :owner_class
8
+
9
+ # Automatically enables eigenclass engine if it is not
10
+ # Returns its engine
11
+ # NOTE: Required by jruby in 1.9 mode. Otherwise inherited
12
+ # eigenclasses don't have their engines
13
+ #
14
+ # @param [Class] eigenclass - class in question
15
+ # @param [Class] owner - owner of eigenclass
16
+ # @return [Engine::Eigenclass]
17
+ def self.lift(eigenclass, owner)
18
+ return Engine.fetch_from(eigenclass) if Engine.applied?(eigenclass)
19
+
20
+ Target.new(eigenclass).apply(Eigenclass)
21
+ eigenclass.extend(MethodDecorators)
22
+ # FIXME; this should detect what user uses `include Contracts` or
23
+ # `include Contracts;;Core`
24
+ eigenclass.send(:include, Contracts)
25
+ Engine.fetch_from(owner).set_eigenclass_owner
26
+ Engine.fetch_from(eigenclass)
27
+ end
28
+
29
+ # No-op for eigenclasses
30
+ def set_eigenclass_owner
31
+ end
32
+
33
+ # Fetches just eigenclasses decorators
34
+ def all_decorators
35
+ pop_decorators
36
+ end
37
+
38
+ private
39
+
40
+ # Fails when contracts are not included in owner class
41
+ def validate!
42
+ fail ContractsNotIncluded unless owner?
43
+ end
44
+
45
+ def owner?
46
+ !!owner_class
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,70 @@
1
+ module Contracts
2
+ module Engine
3
+ # Represents class in question
4
+ class Target
5
+ # Creates new instance of Target
6
+ #
7
+ # @param [Class] target - class in question
8
+ def initialize(target)
9
+ @target = target
10
+ end
11
+
12
+ # Enable contracts engine for target
13
+ # - it is no-op if contracts engine is already enabled
14
+ # - it automatically enables contracts engine for its eigenclass
15
+ # - it sets owner class to target for its eigenclass
16
+ #
17
+ # @param [Engine::Base:Class] engine_class - type of engine to
18
+ # enable (Base or Eigenclass)
19
+ def apply(engine_class = Base)
20
+ return if applied?
21
+
22
+ apply_to_eigenclass
23
+
24
+ eigenclass.class_eval do
25
+ define_method(:__contracts_engine) do
26
+ @__contracts_engine ||= engine_class.new(self)
27
+ end
28
+ end
29
+
30
+ engine.set_eigenclass_owner
31
+ end
32
+
33
+ # Returns true if target has contracts engine already
34
+ #
35
+ # @return [Bool]
36
+ def applied?
37
+ target.respond_to?(:__contracts_engine)
38
+ end
39
+
40
+ # Returns contracts engine of target
41
+ #
42
+ # @return [Engine::Base or Engine::Eigenclass]
43
+ def engine
44
+ applied? && target.__contracts_engine
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :target
50
+
51
+ def apply_to_eigenclass
52
+ return unless meaningless_eigenclass?
53
+
54
+ self.class.new(eigenclass).apply(Eigenclass)
55
+ eigenclass.extend(MethodDecorators)
56
+ # FIXME; this should detect what user uses `include Contracts` or
57
+ # `include Contracts;;Core`
58
+ eigenclass.send(:include, Contracts)
59
+ end
60
+
61
+ def eigenclass
62
+ Support.eigenclass_of(target)
63
+ end
64
+
65
+ def meaningless_eigenclass?
66
+ !Support.eigenclass?(target)
67
+ end
68
+ end
69
+ end
70
+ end