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 =
"0.1.2"
Class Method Summary collapse
- .active?(contract, receiver, args, kwargs) ⇒ Boolean
- .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
- .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
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..fetch(:sample_rate, configuration.sample_rate) rate >= 1 || (rate.positive? && rand < rate) end |
.any(*items) ⇒ Constraints::Union
511 |
# File 'lib/contracts.rb', line 511 def any(*items) = Constraints::Union.new(*items) |
.anything ⇒ Object
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., 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 |
.comparators ⇒ Object
493 |
# File 'lib/contracts.rb', line 493 def comparators = (@comparators ||= {}) |
.configuration ⇒ Object
458 |
# File 'lib/contracts.rb', line 458 def configuration = @configuration ||= Configuration.new |
.configure {|configuration| ... } ⇒ void
This method returns an undefined value.
459 |
# File 'lib/contracts.rb', line 459 def configure = yield(configuration) |
.contract_for(owner, method_name, method_type: :instance) ⇒ Contract?
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
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. 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
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
512 |
# File 'lib/contracts.rb', line 512 def nilable(item) = Constraints::Nilable.new(item) |
.nothing ⇒ Object
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) |
.registry ⇒ Object
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) |