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 =
"0.4.0"
Class Method Summary collapse
- .active?(contract, receiver, args, kwargs) ⇒ Boolean
- .all(*items) ⇒ Object
- .any(*items) ⇒ Constraints::Union
- .anything ⇒ Object
- .array_of(item) ⇒ Object
- .check_invariants(object) ⇒ Object
- .check_invariants!(object) ⇒ Object
- .comparators ⇒ Object
- .configuration ⇒ Object
- .configure {|configuration| ... } ⇒ void
- .contract_for(owner, method_name, method_type: :instance) ⇒ Contract?
- .describe(owner, method_name = nil) ⇒ Object
- .duck_type(*methods) ⇒ Object
- .equal_state?(before, after, comparator = nil) ⇒ Boolean
- .extended(base) ⇒ Object
- .fail!(klass, context, description:, expected: nil, actual: nil, parameter: nil, original_exception: nil) ⇒ Object
- .hash_of(key, value) ⇒ Object
- .included(base) ⇒ Object
- .instrument_violation(error) ⇒ Object
- .invariants_for(owner) ⇒ Object
- .invoke(receiver, contract, args, kwargs, block) ⇒ Object
- .length(min: nil, max: nil, exactly: nil) ⇒ Object
- .matching(regex) ⇒ Object
- .nilable(item) ⇒ Constraints::Nilable
- .nothing ⇒ Object
- .one_of(*values) ⇒ Object
- .predicate(description) ⇒ Object
- .range(value) ⇒ Object
- .register_comparator(name, &block) ⇒ Object
- .registry ⇒ Object
- .respond_to(*methods) ⇒ Object
Class Method Details
.active?(contract, receiver, args, kwargs) ⇒ 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..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
571 |
# File 'lib/contracts.rb', line 571 def any(*items) = Constraints::Union.new(*items) |
.anything ⇒ Object
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., 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 |
.comparators ⇒ Object
553 |
# File 'lib/contracts.rb', line 553 def comparators = (@comparators ||= {}) |
.configuration ⇒ Object
518 |
# File 'lib/contracts.rb', line 518 def configuration = @configuration ||= Configuration.new |
.configure {|configuration| ... } ⇒ void
This method returns an undefined value.
519 |
# File 'lib/contracts.rb', line 519 def configure = yield(configuration) |
.contract_for(owner, method_name, method_type: :instance) ⇒ Contract?
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
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. when :log then configuration.logger&.error(error.) 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
572 |
# File 'lib/contracts.rb', line 572 def nilable(item) = Constraints::Nilable.new(item) |
.nothing ⇒ Object
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) |
.registry ⇒ Object
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) |