Class: Contract
- Inherits:
-
Contracts::Decorator
- Object
- Contracts::Decorator
- Contract
- Defined in:
- lib/contracts.rb
Overview
This is the main Contract class. When you write a new contract, you’ll write it as:
Contract [contract names] => return_value
This class also provides useful callbacks and a validation method.
Constant Summary collapse
- DEFAULT_FAILURE_CALLBACK =
Default implementation of failure_callback. Provided as a block to be able to monkey patch #failure_callback only temporary and then switch it back. First important usage - for specs.
Proc.new do |data| raise data[:contracts].failure_exception.new(failure_msg(data), data) end
Instance Attribute Summary collapse
-
#args_contracts ⇒ Object
readonly
Returns the value of attribute args_contracts.
-
#klass ⇒ Object
readonly
Returns the value of attribute klass.
-
#method ⇒ Object
readonly
Returns the value of attribute method.
-
#ret_contract ⇒ Object
readonly
Returns the value of attribute ret_contract.
Class Method Summary collapse
-
.failure_callback(data, use_pattern_matching = true) ⇒ Object
Callback for when a contract fails.
-
.failure_msg(data) ⇒ Object
Given a hash, prints out a failure message.
- .fetch_failure_callback ⇒ Object
-
.make_validator(contract) ⇒ Object
This is a little weird.
-
.override_failure_callback(&blk) ⇒ Object
Used to override failure_callback without monkeypatching.
-
.restore_failure_callback ⇒ Object
Used to restore default failure callback.
-
.valid?(arg, contract) ⇒ Boolean
Used to verify if an argument satisfies a contract.
Instance Method Summary collapse
- #[](*args, &blk) ⇒ Object
- #call(*args, &blk) ⇒ Object
- #call_with(this, *args, &blk) ⇒ Object
-
#failure_exception ⇒ Object
Used to determine type of failure exception this contract should raise in case of failure.
-
#initialize(klass, method, *contracts) ⇒ Contract
constructor
decorator_name :contract.
-
#pattern_match! ⇒ Object
Used internally to mark contract as pattern matching contract.
-
#pattern_match? ⇒ Boolean
Used to determine if contract is a pattern matching contract.
- #pretty_contract(c) ⇒ Object
- #to_s ⇒ Object
Methods inherited from Contracts::Decorator
Constructor Details
#initialize(klass, method, *contracts) ⇒ Contract
decorator_name :contract
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/contracts.rb', line 96 def initialize(klass, method, *contracts) if contracts[-1].is_a? Hash # internally we just convert that return value syntax back to an array @args_contracts = contracts[0, contracts.size - 1] + contracts[-1].keys @ret_contract = contracts[-1].values[0] @args_validators = @args_contracts.map do |contract| Contract.make_validator(contract) end @ret_validator = Contract.make_validator(@ret_contract) else fail "It looks like your contract for #{method} doesn't have a return value. A contract should be written as `Contract arg1, arg2 => return_value`." end @klass, @method= klass, method @has_func_contracts = args_contracts.index do |contract| contract.is_a? Contracts::Func end end |
Instance Attribute Details
#args_contracts ⇒ Object (readonly)
Returns the value of attribute args_contracts.
94 95 96 |
# File 'lib/contracts.rb', line 94 def args_contracts @args_contracts end |
#klass ⇒ Object (readonly)
Returns the value of attribute klass.
94 95 96 |
# File 'lib/contracts.rb', line 94 def klass @klass end |
#method ⇒ Object (readonly)
Returns the value of attribute method.
94 95 96 |
# File 'lib/contracts.rb', line 94 def method @method end |
#ret_contract ⇒ Object (readonly)
Returns the value of attribute ret_contract.
94 95 96 |
# File 'lib/contracts.rb', line 94 def ret_contract @ret_contract end |
Class Method Details
.failure_callback(data, use_pattern_matching = true) ⇒ Object
Callback for when a contract fails. By default it raises an error and prints detailed info about the contract that failed. You can also monkeypatch this callback to do whatever you want…log the error, send you an email, print an error message, etc.
Example of monkeypatching:
def Contract.failure_callback(data)
puts "You had an error!"
puts failure_msg(data)
exit
end
164 165 166 167 168 169 170 |
# File 'lib/contracts.rb', line 164 def self.failure_callback(data, use_pattern_matching=true) if data[:contracts].pattern_match? && use_pattern_matching return DEFAULT_FAILURE_CALLBACK.call(data) end fetch_failure_callback.call(data) end |
.failure_msg(data) ⇒ Object
Given a hash, prints out a failure message. This function is used by the default #failure_callback method and uses the hash passed into the failure_callback method.
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/contracts.rb', line 127 def self.failure_msg(data) expected = if data[:contract].to_s == "" || data[:contract].is_a?(Hash) data[:contract].inspect else data[:contract].to_s end position = Support.method_position(data[:method]) method_name = Support.method_name(data[:method]) header = if data[:return_value] "Contract violation for return value:" else "Contract violation for argument #{data[:arg_pos]} of #{data[:total_args]}:" end %{#{header} Expected: #{expected}, Actual: #{data[:arg].inspect} Value guarded in: #{data[:class]}::#{method_name} With Contract: #{data[:contracts]} At: #{position} } end |
.fetch_failure_callback ⇒ Object
192 193 194 |
# File 'lib/contracts.rb', line 192 def self.fetch_failure_callback @failure_callback ||= DEFAULT_FAILURE_CALLBACK end |
.make_validator(contract) ⇒ Object
This is a little weird. For each contract we pre-make a proc to validate it so we don’t have to go through this decision tree every time. Seems silly but it saves us a bunch of time (4.3sec vs 5.2sec)
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 |
# File 'lib/contracts.rb', line 211 def self.make_validator(contract) # if is faster than case! klass = contract.class if klass == Proc # e.g. lambda {true} contract elsif klass == Array # e.g. [Num, String] # TODO account for these errors too lambda { |arg| return false unless arg.is_a?(Array) && arg.length == contract.length arg.zip(contract).all? do |_arg, _contract| Contract.valid?(_arg, _contract) end } elsif klass == Hash # e.g. { :a => Num, :b => String } lambda { |arg| return false unless arg.is_a?(Hash) contract.keys.all? do |k| Contract.valid?(arg[k], contract[k]) end } elsif klass == Contracts::Args lambda { |arg| Contract.valid?(arg, contract.contract) } elsif klass == Contracts::Func lambda { |arg| arg.is_a?(Method) || arg.is_a?(Proc) } else # classes and everything else # e.g. Fixnum, Num if contract.respond_to? :valid? lambda { |arg| contract.valid?(arg) } elsif klass == Class lambda { |arg| arg.is_a?(contract) } else lambda { |arg| contract == arg } end end end |
.override_failure_callback(&blk) ⇒ Object
Used to override failure_callback without monkeypatching.
Takes: block parameter, that should accept one argument - data.
Example usage:
Contract.override_failure_callback do |data|
puts "You had an error"
puts failure_msg(data)
exit
end
183 184 185 |
# File 'lib/contracts.rb', line 183 def self.override_failure_callback(&blk) @failure_callback = blk end |
.restore_failure_callback ⇒ Object
Used to restore default failure callback
188 189 190 |
# File 'lib/contracts.rb', line 188 def self.restore_failure_callback @failure_callback = DEFAULT_FAILURE_CALLBACK end |
.valid?(arg, contract) ⇒ Boolean
Used to verify if an argument satisfies a contract.
Takes: an argument and a contract.
Returns: a tuple: [Boolean, metadata]. The boolean indicates whether the contract was valid or not. If it wasn’t, metadata contains some useful information about the failure.
203 204 205 |
# File 'lib/contracts.rb', line 203 def self.valid?(arg, contract) make_validator(contract)[arg] end |
Instance Method Details
#[](*args, &blk) ⇒ Object
255 256 257 |
# File 'lib/contracts.rb', line 255 def [](*args, &blk) call(*args, &blk) end |
#call(*args, &blk) ⇒ Object
259 260 261 |
# File 'lib/contracts.rb', line 259 def call(*args, &blk) call_with(nil, *args, &blk) end |
#call_with(this, *args, &blk) ⇒ Object
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 |
# File 'lib/contracts.rb', line 263 def call_with(this, *args, &blk) _args = blk ? args + [blk] : args # check contracts on arguments # fun fact! This is significantly faster than .zip (3.7 secs vs 4.7 secs). Why?? last_index = @args_validators.size - 1 # times is faster than (0..args.size).each _args.size.times do |i| # this is done to account for extra args (for *args) j = i < last_index ? i : last_index #unless true #@args_contracts[i].valid?(args[i]) unless @args_validators[j][_args[i]] call_function = Contract.failure_callback({:arg => _args[i], :contract => @args_contracts[j], :class => @klass, :method => @method, :contracts => self, :arg_pos => i+1, :total_args => _args.size}) return unless call_function end end if @has_func_contracts # contracts on methods @args_contracts.each_with_index do |contract, i| if contract.is_a? Contracts::Func args[i] = Contract.new(@klass, args[i], *contract.contracts) end end end result = if @method.respond_to? :bind # instance method @method.bind(this).call(*args, &blk) else # class method @method.call(*args, &blk) end unless @ret_validator[result] Contract.failure_callback({:arg => result, :contract => @ret_contract, :class => @klass, :method => @method, :contracts => self, :return_value => true}) end this.verify_invariants!(@method) if this.respond_to?(:verify_invariants!) result end |
#failure_exception ⇒ Object
Used to determine type of failure exception this contract should raise in case of failure
307 308 309 310 311 312 313 |
# File 'lib/contracts.rb', line 307 def failure_exception if @pattern_match PatternMatchingError else ContractError end end |
#pattern_match! ⇒ Object
Used internally to mark contract as pattern matching contract
317 318 319 |
# File 'lib/contracts.rb', line 317 def pattern_match! @pattern_match = true end |
#pattern_match? ⇒ Boolean
Used to determine if contract is a pattern matching contract
322 323 324 |
# File 'lib/contracts.rb', line 322 def pattern_match? @pattern_match end |
#pretty_contract(c) ⇒ Object
114 115 116 |
# File 'lib/contracts.rb', line 114 def pretty_contract c c.is_a?(Class) ? c.name : c.class.name end |
#to_s ⇒ Object
118 119 120 121 122 |
# File 'lib/contracts.rb', line 118 def to_s args = @args_contracts.map { |c| pretty_contract(c) }.join(", ") ret = pretty_contract(@ret_contract) ("#{args} => #{ret}").gsub("Contracts::", "") end |