Class: Module

Inherits:
Object
  • Object
show all
Defined in:
lib/contract/integration.rb

Instance Method Summary collapse

Instance Method Details

#fulfills(*contracts) ⇒ 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.



436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
# File 'lib/contract/integration.rb', line 436

def fulfills(*contracts)
  return unless Contract.check_fulfills?

  contracts.each do |contract|
    contract.implications.each do |implication|
      include implication
    end
  end

  old_method = instance_method(:initialize)
  remove_method(:initialize) if instance_methods(false).include?("initialize")

  # Keep visible references around so that the GC will not eat these up.
  @fulfills ||= Array.new
  @fulfills << [contracts, old_method]

  # Have to use class_eval because define_method does not allow methods to take
  # blocks. This can be cleaned up when Ruby 1.9 has become current.
  class_eval %{
    def initialize(*args, &block)
      ObjectSpace._id2ref(#{old_method.object_id}).bind(self).call(*args, &block)
      ObjectSpace._id2ref(#{contracts.object_id}).each do |contract|
        contract.enforce self
      end
    end
  }, "(post initialization contract check for #{self.inspect})"

  return true
end

#signature(method, *args) ⇒ Object

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


237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
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
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/contract/integration.rb', line 237

def signature(method, *args)
  options = {}
  signature = args.dup
  options.update(signature.pop) if signature.last.is_a?(Hash)
  method = method.to_sym

  return if not Contract.check_signatures? and options[:no_adaption]

  old_method = instance_method(method)
  remove_method(method) if instance_methods(false).include?(method.to_s)

  arity = old_method.arity
  if arity != signature.size and
    (arity >= 0 or signature.size < ~arity) then
    raise(ArgumentError, "signature isn't compatible with arity")
  end

  # Normalizes specifiers to Objects that respond to === so that the run-time
  # checks only have to deal with that case. Also checks that a specifier is
  # actually valid.
  convert_specifier = lambda do |item|
    # Procs, Methods etc.
    if item.respond_to?(:call) then
      Contract::Check.block { |arg| item.call(arg) }
    # Already okay
    elsif item.respond_to?(:===) or item == :any then
      item
    # Unknown specifier
    else
      raise(ArgumentError, "unsupported argument specifier #{item.inspect}")
    end
  end

  signature.map!(&convert_specifier)

  if options.include?(:optional) then
    options[:optional] = Array(options[:optional])
    options[:optional].map!(&convert_specifier)
    options.delete(:optional) if options[:optional].empty?
  end

  if options.include?(:repeated) then
    options[:repeated] = Array(options[:repeated])
    if options[:repeated].size == 0 then
      raise(ArgumentError, "repeated arguments may not be an empty Array")
    else
      options[:repeated].map!(&convert_specifier)
    end
  end

  if options.include?(:return) then
    options[:return] = convert_specifier.call(options[:return])
  end

  # We need to keep around references to our arguments because we will
  # need to access them via ObjectSpace._id2ref so that they do not
  # get garbage collected.
  @signatures ||= Hash.new { |hash, key| hash[key] = Array.new }
  @signatures[method] << [signature, options, old_method]

  adapted = Proc.new do |obj, type, assign_to|
    if options[:no_adaption] then
      obj
    elsif assign_to then
      %{(#{assign_to} = Contract.adapt(#{obj}, #{type}))}
    else
      %{Contract.adapt(#{obj}, #{type})}
    end
  end

  # We have to use class_eval so that signatures can be specified for
  # methods taking blocks in Ruby 1.8. (This will be obsolete in 1.9)
  # We also make the checks as efficient as we can.
  code = %{
    def #{method}(*args, &block)
      old_args = args.dup

      #{if options.include?(:block) then
          if options[:block] then
            %{raise(ArgumentError, "no block given") unless block}
          else
            %{raise(ArgumentError, "block given") if block}
          end
        end
      }

      #{if not (options[:allow_trailing] or
          options.include?(:repeated) or options.include?(:optional))
        then
          msg = "wrong number of arguments (\#{args.size} for " +
            "#{signature.size})"
          %{if args.size != #{signature.size} then
              raise(ArgumentError, "#{msg}")
            end
          }
        elsif options.include?(:optional) and
          not options.include?(:allow_trailing) and
          not options.include?(:repeated)
        then
          min = signature.size
          max = signature.size + options[:optional].size
          msg = "wrong number of arguments (\#{args.size} for " +
            "#{min} upto #{max})"
          %{unless args.size.between?(#{min}, #{max})
              raise(ArgumentError, "#{msg}")
            end
          }
        elsif signature.size > 0 then
          msg = "wrong number of arguments (\#{args.size} for " +
            "at least #{signature.size}"
          %{if args.size < #{signature.size} then
              raise(ArgumentError, "#{msg}")
            end
          }
        end
      }

      #{index = 0
        signature.map do |part|
          next if part == :any
          index += 1
          msg = "argument #{index} (\#{arg.inspect}) does not match " +
            "#{part.inspect}"
          %{type = ObjectSpace._id2ref(#{part.object_id})
            arg = args.shift
            unless type === #{adapted[%{arg}, %{type}, %{old_args[#{index - 1}]}]}
              raise(ArgumentError, "#{msg}")
            end
          }
        end
      }

      #{%{catch(:args_exhausted) do} if options.include?(:optional)}
        #{if optional = options[:optional] then
            index = 0
            optional.map do |part|
              next if part == :any
              index += 1
              msg = "argument #{index + signature.size} " +
                "(\#{arg.inspect}) does not match #{part.inspect}"
              oa_index = index + signature.size - 1

              %{throw(:args_exhausted) if args.empty?
                type = ObjectSpace._id2ref(#{part.object_id})
                arg = args.shift
                unless type === #{adapted[%{arg}, %{type}, %{old_args[#{oa_index}]}]}
                  raise(ArgumentError, "#{msg}")
                end
              }
            end
          end
        }

        #{if repeated = options[:repeated] then
            arg_off = 1 + signature.size
            arg_off += options[:optional].size if options.include?(:optional)
            msg = "argument \#{idx + #{arg_off}} " +
              "(\#{arg.inspect}) does not match \#{part.inspect}"
            %{parts = ObjectSpace._id2ref(#{repeated.object_id})
              args.each_with_index do |arg, idx|
                part = parts[idx % #{repeated.size}]
                if part != :any and
                  not part === (#{adapted[%{arg}, %{part}, %{old_args[idx]}]})
                then
                  raise(ArgumentError, "#{msg}")
                end
              end
            }
          end
        }
      #{%{end} if options.include?(:optional)}

      result = ObjectSpace._id2ref(#{old_method.object_id}).bind(self).
        call(*old_args, &block)
      #{if rt = options[:return] and rt != :any then
          msg = "return value (\#{result.inspect}) does not match #{rt.inspect}"
          %{type = ObjectSpace._id2ref(#{rt.object_id})
            unless type === #{adapted[%{result}, %{type}]}
              raise(StandardError, "#{msg}")
            end
          }
        end
      }
    end
  }
  class_eval code, "(signature check for #{old_method.inspect[/: (.+?)>\Z/, 1]})"

  return true
end