Class | Module |
In: |
lib/contract/integration.rb
|
Parent: | Object |
Specifies that this Module/Class fulfills one or more contracts. The contracts will automatically be verified after an instance has been successfully created. This only actually does the checks when Contract.check_fulfills is enabled. The method will return true in case it actually inserted the check logic and nil in case it didn’t.
Note that this works by overriding the initialize method which means that you should either add the fulfills statements after your initialize method or call the previously defined initialize method from your new one.
# File lib/contract/integration.rb, line 436 436: def fulfills(*contracts) 437: return unless Contract.check_fulfills? 438: 439: contracts.each do |contract| 440: contract.implications.each do |implication| 441: include implication 442: end 443: end 444: 445: old_method = instance_method(:initialize) 446: remove_method(:initialize) if instance_methods(false).include?("initialize") 447: 448: # Keep visible references around so that the GC will not eat these up. 449: @fulfills ||= Array.new 450: @fulfills << [contracts, old_method] 451: 452: # Have to use class_eval because define_method does not allow methods to take 453: # blocks. This can be cleaned up when Ruby 1.9 has become current. 454: class_eval %{ 455: def initialize(*args, &block) 456: ObjectSpace._id2ref(#{old_method.object_id}).bind(self).call(*args, &block) 457: ObjectSpace._id2ref(#{contracts.object_id}).each do |contract| 458: contract.enforce self 459: end 460: end 461: }, "(post initialization contract check for #{self.inspect})" 462: 463: return true 464: end
Checks that the arguments and return value of a method match the specified signature. Checks are only actually done when Contract.check_signatures is set to true or if the :no_adaption option is false. The method will return true in case it actually inserted the signature check logic and nil in case it didn’t.
You will usually specify one type specifier (:any which will allow anything to appear at that position of the argument list or something that implements the === case equality operator — samples are Contracts, Ranges, Classes, Modules, Regexps, Contract::Checks and so on) per argument. You can also use objects that implement the call method as type specifiers which includes Methods and Procs.
If you don’t use the :repeated or :allow_trailing options the method will take exactly as many arguments as there are type specifiers which means that signature :a_method enforces a_method having exactly zero arguments.
The checks are done by wrapping the type checks around the method. ArgumentError exceptions will be raised in case the signature contract is not adhered to by your caller.
An ArgumentError exception will be raised in case the methods natural argument list size and the signature you specified via Module.signature are incompatible. (Note that they don’t have to be completely equivalent, you can still have a method taking zero or more arguments and apply a signature that limits the actual argument count to three arguments.)
This method can take quite a few options. Here’s a complete list:
:return: | A return type that the method must comply to. Note that this check (if failed) will actually raise a StandardError instead of an ArgumentError because the failure likely lies in the method itself and not in what the caller did. |
:block: | true or false — whether the method must take a block or not. So specifying :block => false enforces that the method is not allowed to have a block supplied. |
:allow_trailing: | true or false — whether the argument list may contain trailing, unchecked arguments. |
:optional: | An Array specifying optional arguments. These arguments are assumed to be
after regular arguments, but before repeated ones. They will be
checked if they are present, but don’t actually have to be present.
This could for example be useful for File.open(name, mode) where mode is optional, but has to be either an Integer or String. Note that all optional arguments will have to be specified if you want to use optional and repeated arguments. Specifying an empty Array is like not supplying the option at all. |
:repeated: | An Array that specifies arguments of a method that will be repeated over
and over again at the end of the argument list.
A good sample of this are Array#values_at which takes zero or or more Numeric arguments and Enumerable#zip which takes zero or more other Enumerable arguments. Note that the Array that was associated with the :repeated option must not be empty or an ArgumentError exception will be raised. If there’s just one repeated type you can omit the Array and directly specify the type identifier. The :repeated option overrides the :allow_trailing option. Combining them is thus quite meaningless. |
:no_adaption: | true or false — whether no type adaption should be performed. |
Usage:
signature(:to_s) # no arguments signature(:+, :any) # one argument, type unchecked signature(:+, Fixnum) # one argument, type Fixnum signature(:+, NumericContract) signature(:+, 1 .. 10) signature(:sqrt, lambda { |arg| arg > 0 }) signature(:each, :block => true) # has to have block signature(:to_i, :block => false) # not allowed to have block signature(:to_i, :result => Fixnum) # return value must be Fixnum signature(:zip, :allow_trailing => true) # unchecked trailing args signature(:zip, :repeated => [Enumerable]) # repeated trailing args signature(:zip, :repeated => Enumerable) # foo(3, 6, 4, 7) works; foo(5), foo(3, 2) etc. don't signature(:foo, :repeated => [1..4, 5..9]) signature(:foo, :optional => [Numeric, String]) # two optional args
# File lib/contract/integration.rb, line 237 237: def signature(method, *args) 238: options = {} 239: signature = args.dup 240: options.update(signature.pop) if signature.last.is_a?(Hash) 241: method = method.to_sym 242: 243: return if not Contract.check_signatures? and options[:no_adaption] 244: 245: old_method = instance_method(method) 246: remove_method(method) if instance_methods(false).include?(method.to_s) 247: 248: arity = old_method.arity 249: if arity != signature.size and 250: (arity >= 0 or signature.size < ~arity) then 251: raise(ArgumentError, "signature isn't compatible with arity") 252: end 253: 254: # Normalizes specifiers to Objects that respond to === so that the run-time 255: # checks only have to deal with that case. Also checks that a specifier is 256: # actually valid. 257: convert_specifier = lambda do |item| 258: # Procs, Methods etc. 259: if item.respond_to?(:call) then 260: Contract::Check.block { |arg| item.call(arg) } 261: # Already okay 262: elsif item.respond_to?(:===) or item == :any then 263: item 264: # Unknown specifier 265: else 266: raise(ArgumentError, "unsupported argument specifier #{item.inspect}") 267: end 268: end 269: 270: signature.map!(&convert_specifier) 271: 272: if options.include?(:optional) then 273: options[:optional] = Array(options[:optional]) 274: options[:optional].map!(&convert_specifier) 275: options.delete(:optional) if options[:optional].empty? 276: end 277: 278: if options.include?(:repeated) then 279: options[:repeated] = Array(options[:repeated]) 280: if options[:repeated].size == 0 then 281: raise(ArgumentError, "repeated arguments may not be an empty Array") 282: else 283: options[:repeated].map!(&convert_specifier) 284: end 285: end 286: 287: if options.include?(:return) then 288: options[:return] = convert_specifier.call(options[:return]) 289: end 290: 291: # We need to keep around references to our arguments because we will 292: # need to access them via ObjectSpace._id2ref so that they do not 293: # get garbage collected. 294: @signatures ||= Hash.new { |hash, key| hash[key] = Array.new } 295: @signatures[method] << [signature, options, old_method] 296: 297: adapted = Proc.new do |obj, type, assign_to| 298: if options[:no_adaption] then 299: obj 300: elsif assign_to then 301: %{(#{assign_to} = Contract.adapt(#{obj}, #{type}))} 302: else 303: %{Contract.adapt(#{obj}, #{type})} 304: end 305: end 306: 307: # We have to use class_eval so that signatures can be specified for 308: # methods taking blocks in Ruby 1.8. (This will be obsolete in 1.9) 309: # We also make the checks as efficient as we can. 310: code = %{ 311: def #{method}(*args, &block) 312: old_args = args.dup 313: 314: #{if options.include?(:block) then 315: if options[:block] then 316: %{raise(ArgumentError, "no block given") unless block} 317: else 318: %{raise(ArgumentError, "block given") if block} 319: end 320: end 321: } 322: 323: #{if not (options[:allow_trailing] or 324: options.include?(:repeated) or options.include?(:optional)) 325: then 326: msg = "wrong number of arguments (\#{args.size} for " + 327: "#{signature.size})" 328: %{if args.size != #{signature.size} then 329: raise(ArgumentError, "#{msg}") 330: end 331: } 332: elsif options.include?(:optional) and 333: not options.include?(:allow_trailing) and 334: not options.include?(:repeated) 335: then 336: min = signature.size 337: max = signature.size + options[:optional].size 338: msg = "wrong number of arguments (\#{args.size} for " + 339: "#{min} upto #{max})" 340: %{unless args.size.between?(#{min}, #{max}) 341: raise(ArgumentError, "#{msg}") 342: end 343: } 344: elsif signature.size > 0 then 345: msg = "wrong number of arguments (\#{args.size} for " + 346: "at least #{signature.size}" 347: %{if args.size < #{signature.size} then 348: raise(ArgumentError, "#{msg}") 349: end 350: } 351: end 352: } 353: 354: #{index = 0 355: signature.map do |part| 356: next if part == :any 357: index += 1 358: msg = "argument #{index} (\#{arg.inspect}) does not match " + 359: "#{part.inspect}" 360: %{type = ObjectSpace._id2ref(#{part.object_id}) 361: arg = args.shift 362: unless type === #{adapted[%{arg}, %{type}, %{old_args[#{index - 1}]}]} 363: raise(ArgumentError, "#{msg}") 364: end 365: } 366: end 367: } 368: 369: #{%{catch(:args_exhausted) do} if options.include?(:optional)} 370: #{if optional = options[:optional] then 371: index = 0 372: optional.map do |part| 373: next if part == :any 374: index += 1 375: msg = "argument #{index + signature.size} " + 376: "(\#{arg.inspect}) does not match #{part.inspect}" 377: oa_index = index + signature.size - 1 378: 379: %{throw(:args_exhausted) if args.empty? 380: type = ObjectSpace._id2ref(#{part.object_id}) 381: arg = args.shift 382: unless type === #{adapted[%{arg}, %{type}, %{old_args[#{oa_index}]}]} 383: raise(ArgumentError, "#{msg}") 384: end 385: } 386: end 387: end 388: } 389: 390: #{if repeated = options[:repeated] then 391: arg_off = 1 + signature.size 392: arg_off += options[:optional].size if options.include?(:optional) 393: msg = "argument \#{idx + #{arg_off}} " + 394: "(\#{arg.inspect}) does not match \#{part.inspect}" 395: %{parts = ObjectSpace._id2ref(#{repeated.object_id}) 396: args.each_with_index do |arg, idx| 397: part = parts[idx % #{repeated.size}] 398: if part != :any and 399: not part === (#{adapted[%{arg}, %{part}, %{old_args[idx]}]}) 400: then 401: raise(ArgumentError, "#{msg}") 402: end 403: end 404: } 405: end 406: } 407: #{%{end} if options.include?(:optional)} 408: 409: result = ObjectSpace._id2ref(#{old_method.object_id}).bind(self). 410: call(*old_args, &block) 411: #{if rt = options[:return] and rt != :any then 412: msg = "return value (\#{result.inspect}) does not match #{rt.inspect}" 413: %{type = ObjectSpace._id2ref(#{rt.object_id}) 414: unless type === #{adapted[%{result}, %{type}]} 415: raise(StandardError, "#{msg}") 416: end 417: } 418: end 419: } 420: end 421: } 422: class_eval code, "(signature check for #{old_method.inspect[/: (.+?)>\Z/, 1]})" 423: 424: return true 425: end