Module: Contracts

Defined in:
lib/contracts.rb,
lib/contracts/version.rb,
lib/contracts/rspec/verifier.rb,
sig/contracts.rbs

Overview

Runtime behavioral contracts. Include in classes or extend for singleton contracts.

Defined Under Namespace

Modules: ClassMethods, Constraints, InstanceMethods, RSpec, SingletonClassMethods Classes: CompositeViolation, Condition, Configuration, Context, Contract, ContractBuilder, DefinitionError, Error, ExceptionRule, ExecutionGuard, Invariant, MutationReport, Observation, Registry, Snapshot, SnapshotError, StateObservationError, Violation

Constant Summary collapse

SENSITIVE_NAMES =
[/password/i, /token/i, /secret/i, /authorization/i, /api_key/i, /access_key/i, /credit_card/i,
/ssn/i].freeze
VERSION =

Returns:

  • (String)
"0.1.2"

Class Method Summary collapse

Class Method Details

.active?(contract, receiver, args, kwargs) ⇒ Boolean

Returns:

  • (Boolean)


572
573
574
575
576
577
578
579
580
581
# File 'lib/contracts.rb', line 572

def active?(contract, receiver, args, kwargs)
  return false unless configuration.enabled
  if configuration.sampler
    return configuration.sampler.call(Context.new(receiver: receiver, contract: contract, arguments: args,
                                                  keyword_arguments: kwargs, block_given: false))
  end

  rate = contract.options.fetch(:sample_rate, configuration.sample_rate)
  rate >= 1 || (rate.positive? && rand < rate)
end

.any(*items) ⇒ Constraints::Union

Parameters:

  • values (Object)

Returns:



511
# File 'lib/contracts.rb', line 511

def any(*items) = Constraints::Union.new(*items)

.anythingObject



521
# File 'lib/contracts.rb', line 521

def anything = Constraints::Anything.new

.array_of(item) ⇒ Object



516
# File 'lib/contracts.rb', line 516

def array_of(item) = Constraints::ArrayOf.new(item)

.check_invariants(object) ⇒ Object



473
474
475
476
477
478
479
480
# File 'lib/contracts.rb', line 473

def check_invariants(object)
  invariants_for(object.class).map do |invariant|
    { passed: !!object.instance_exec(&invariant.predicate), type: :invariant, description: invariant.description,
      invariant_id: invariant.id }.freeze
  end.freeze
rescue StandardError => e
  [{ passed: false, type: :invariant, description: e.message, error: e }.freeze].freeze
end

.check_invariants!(object) ⇒ Object



482
483
484
485
486
487
488
489
490
# File 'lib/contracts.rb', line 482

def check_invariants!(object)
  failed = check_invariants(object).find { |result| !result[:passed] }
  if failed
    raise InvariantViolation.new(owner: object.class, method_name: :__invariant__, contract_type: :invariant,
                                 description: failed[:description])
  end

  true
end

.comparatorsObject



493
# File 'lib/contracts.rb', line 493

def comparators = (@comparators ||= {})

.configurationObject



458
# File 'lib/contracts.rb', line 458

def configuration = @configuration ||= Configuration.new

.configure {|configuration| ... } ⇒ void

This method returns an undefined value.

Yields:

Yield Parameters:

Yield Returns:

  • (void)


459
# File 'lib/contracts.rb', line 459

def configure = yield(configuration)

.contract_for(owner, method_name, method_type: :instance) ⇒ Contract?

Parameters:

  • owner (Module)
  • method_name (Symbol)
  • method_type: (Symbol) (defaults to: :instance)

Returns:



462
463
464
465
# File 'lib/contracts.rb', line 462

def contract_for(owner, method_name,
                 method_type: :instance)
  registry.find(owner, method_name, method_type: method_type)
end

.describe(owner, method_name = nil) ⇒ Object



506
507
508
509
# File 'lib/contracts.rb', line 506

def describe(owner, method_name = nil)
  contracts = method_name ? [contract_for(owner, method_name)].compact : registry.for_class(owner)
  contracts.map(&:to_h)
end

.duck_type(*methods) ⇒ Object



520
# File 'lib/contracts.rb', line 520

def duck_type(*methods) = Constraints::DuckType.new(*methods)

.equal_state?(before, after, comparator = nil) ⇒ Boolean

Returns:

  • (Boolean)


495
496
497
498
499
500
501
502
503
504
# File 'lib/contracts.rb', line 495

def equal_state?(before, after, comparator = nil)
  comparator = comparators[comparator] if comparator.is_a?(Symbol)
  return comparator.call(before, after) if comparator.respond_to?(:call)

  if configuration.state_equality == :identity
    before.equal?(after)
  else
    configuration.state_equality == :equal ? before == after : before.eql?(after)
  end
end

.extended(base) ⇒ Object



822
# File 'lib/contracts.rb', line 822

def self.extended(base) = base.extend(SingletonClassMethods)

.fail!(klass, context, description:, expected: nil, actual: nil, parameter: nil, original_exception: nil) ⇒ Object



583
584
585
586
587
588
589
590
591
592
593
594
595
# File 'lib/contracts.rb', line 583

def fail!(klass, context, description:, expected: nil, actual: nil, parameter: nil, original_exception: nil)
  error = klass.new(owner: context.owner, method_name: context.method_name,
                    contract_type: klass.name.split("::").last.sub("Violation", "").downcase, description: description, expected: expected, actual: actual, parameter: parameter, context: context, source_location: context.source_location, original_exception: original_exception)
  instrument_violation(error)
  case configuration.failure_mode
  when :raise then raise error
  when :warn then warn error.message
  when :log then configuration.logger&.error(error.message)
  when :collect then (Thread.current[:contracts_violations] ||= []) << error
  else raise DefinitionError, "unknown failure_mode #{configuration.failure_mode.inspect}"
  end
  error
end

.hash_of(key, value) ⇒ Object



517
# File 'lib/contracts.rb', line 517

def hash_of(key, value) = Constraints::HashOf.new(key, value)

.included(base) ⇒ Object



817
818
819
820
# File 'lib/contracts.rb', line 817

def self.included(base)
  base.extend(ClassMethods)
  base.include(InstanceMethods)
end

.instrument_violation(error) ⇒ Object



597
598
599
600
601
602
603
604
605
606
607
608
609
# File 'lib/contracts.rb', line 597

def instrument_violation(error)
  return unless defined?(ActiveSupport::Notifications)

  ActiveSupport::Notifications.instrument(
    "contracts.violation",
    owner: error.owner,
    method_name: error.method_name,
    contract_type: error.contract_type,
    description: error.description,
    duration: error.context&.duration,
    source_location: error.source_location
  )
end

.invariants_for(owner) ⇒ Object



467
468
469
470
471
# File 'lib/contracts.rb', line 467

def invariants_for(owner)
  owner.ancestors.flat_map do |ancestor|
    registry.for_class(ancestor).flat_map(&:invariants)
  end.freeze
end

.invoke(receiver, contract, args, kwargs, block) ⇒ Object



524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
# File 'lib/contracts.rb', line 524

def invoke(receiver, contract, args, kwargs, block)
  return yield unless active?(contract, receiver, args, kwargs)

  parent = Thread.current[:contracts_context]

  context = Context.new(receiver: receiver, contract: contract, arguments: args, keyword_arguments: kwargs,
                        block_given: !block.nil?, parent: parent)
  Thread.current[:contracts_context] = context
  validate_parameters(contract, context)

  check_contract_invariants(receiver, contract, context, :before)
  context.before = capture(receiver, contract)
  check_conditions(contract.preconditions, context, :precondition)
  begin
    context.result = yield
  rescue Exception => e # rubocop:disable Lint/RescueException
    context.exception = e
    if configuration.verify_state_after_exception || !contract.unchanged_on_raise_types.empty?
      after = capture(receiver, contract)
      report = MutationReport.new(before: context.before, after: after, permitted: [], required: [], observations: contract.observed.to_h do |o|
        [o.name, o]
      end)
      if !contract.unchanged_on_raise_types.empty? && contract.unchanged_on_raise_types.any? do |type|
        e.is_a?(type)
      end && !report.changed_fields.empty?
        fail!(MutationViolation, context,
              description: "state changed after exception: #{report.changed_fields.join(', ')}", actual: report.to_h, original_exception: e)
      end
      check_contract_invariants(receiver, contract, context, :after_exception)
    end
    handle_exception(contract, context)

    check_contract_invariants(receiver, contract, context, :after) if configuration.check_invariant_after_exception
    raise
  else
    validate_return(contract, context)

    check_conditions(contract.postconditions, context, :postcondition)
    validate_mutation(contract, context)
    check_contract_invariants(receiver, contract, context, :after)
    context.result
  ensure
    context.finished_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)

    Thread.current[:contracts_context] = parent
  end
end

.matching(regex) ⇒ Object



513
# File 'lib/contracts.rb', line 513

def matching(regex) = Constraints::Regex.new(regex)

.nilable(item) ⇒ Constraints::Nilable

Parameters:

  • value (Object)

Returns:



512
# File 'lib/contracts.rb', line 512

def nilable(item) = Constraints::Nilable.new(item)

.nothingObject



522
# File 'lib/contracts.rb', line 522

def nothing = Constraints::Nothing.new

.one_of(*values) ⇒ Object



515
# File 'lib/contracts.rb', line 515

def one_of(*values) = Constraints::OneOf.new(*values)

.predicate(description) ⇒ Object



518
# File 'lib/contracts.rb', line 518

def predicate(description, &) = Constraints::Predicate.new(description, &)

.range(value) ⇒ Object



514
# File 'lib/contracts.rb', line 514

def range(value) = Constraints::Range.new(value)

.register_comparator(name, &block) ⇒ Object



492
# File 'lib/contracts.rb', line 492

def register_comparator(name, &block) = (comparators[name.to_sym] = block)

.registryObject



460
# File 'lib/contracts.rb', line 460

def registry = @registry ||= Registry.new

.respond_to(*methods) ⇒ Object



519
# File 'lib/contracts.rb', line 519

def respond_to(*methods) = Constraints::RespondTo.new(*methods)