Module: Contracts

Defined in:
lib/contracts.rb,
lib/contracts/version.rb,
lib/contracts/structured.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.4.0"

Class Method Summary collapse

Class Method Details

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

Returns:

  • (Boolean)


634
635
636
637
638
639
640
641
642
643
# File 'lib/contracts.rb', line 634

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

.all(*items) ⇒ Object



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

def all(*items) = Constraints::All.new(*items)

.any(*items) ⇒ Constraints::Union

Parameters:

  • values (Object)

Returns:



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

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

.anythingObject



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

def anything = Constraints::Anything.new

.array_of(item) ⇒ Object



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

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

.check_invariants(object) ⇒ Object



533
534
535
536
537
538
539
540
# File 'lib/contracts.rb', line 533

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



542
543
544
545
546
547
548
549
550
# File 'lib/contracts.rb', line 542

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



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

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

.configurationObject



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

def configuration = @configuration ||= Configuration.new

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

This method returns an undefined value.

Yields:

Yield Parameters:

Yield Returns:

  • (void)


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

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:



522
523
524
525
# File 'lib/contracts.rb', line 522

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



566
567
568
569
# File 'lib/contracts.rb', line 566

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



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

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

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

Returns:

  • (Boolean)


555
556
557
558
559
560
561
562
563
564
# File 'lib/contracts.rb', line 555

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



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

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

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



645
646
647
648
649
650
651
652
653
654
655
656
657
# File 'lib/contracts.rb', line 645

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



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

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

.included(base) ⇒ Object



909
910
911
912
# File 'lib/contracts.rb', line 909

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

.instrument_violation(error) ⇒ Object



659
660
661
662
663
664
665
666
667
668
669
670
671
# File 'lib/contracts.rb', line 659

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



527
528
529
530
531
# File 'lib/contracts.rb', line 527

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



586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
# File 'lib/contracts.rb', line 586

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

.length(min: nil, max: nil, exactly: nil) ⇒ Object



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

def length(min: nil, max: nil, exactly: nil) = Constraints::Length.new(min: min, max: max, exactly: exactly)

.matching(regex) ⇒ Object



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

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

.nilable(item) ⇒ Constraints::Nilable

Parameters:

  • value (Object)

Returns:



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

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

.nothingObject



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

def nothing = Constraints::Nothing.new

.one_of(*values) ⇒ Object



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

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

.predicate(description) ⇒ Object



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

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

.range(value) ⇒ Object



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

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

.register_comparator(name, &block) ⇒ Object



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

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

.registryObject



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

def registry = @registry ||= Registry.new

.respond_to(*methods) ⇒ Object



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

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