Class: Stannum::Contract

Inherits:
Stannum::Contracts::Base show all
Defined in:
lib/stannum/contract.rb

Overview

A Contract defines constraints on an object and its properties.

Examples:

Creating A Contract With Property Constraints

Widget = Struct.new(:name, :manufacturer)
Manufacturer = Struct.new(:factory)
Factory = Struct.new(:address)

type_constraint = Stannum::Constraints::Type.new(Widget)
name_constraint =
  Stannum::Constraint.new(type: 'wrong_name', negated_type: 'right_name') do |value|
    value == 'Self-sealing Stem Bolt'
  end
address_constraint =
  Stannum::Constraint.new(type: 'wrong_address', negated_type: 'right_address') do |value|
    value == '123 Example Street'
  end
contract =
  Stannum::Contract.new
  .add_constraint(type_constraint)
  .add_constraint(name_constraint, property: :name)
  .add_constraint(address_constraint, property: %i[manufacturer factory address])

With An Object That Matches None Of The Property Constraints

# With a non-Widget object.
contract.matches?(nil) #=> false
errors = contract.errors_for(nil)
errors.to_a
#=> [
  { type: 'is_not_type', data: { type: Widget }, path: [], message: nil },
  { type: 'wrong_name', data: {}, path: [:name], message: nil },
  { type: 'wrong_address', data: {}, path: [:manufacturer, :factory, :address], message: nil }
]
errors[:name].to_a
#=> [
  { type: 'wrong_name', data: {}, path: [], message: nil }
]
errors[:manufacturer].to_a
#=> [
  { type: 'wrong_address', data: {}, path: [:factory, :address], message: nil }
]

contract.does_not_match?(nil)          #=> true
contract.negated_errors_for?(nil).to_a #=> []

With An Object That Matches Some Of The Property Constraints

contract.matches?(Widget.new) #=> false
errors = contract.errors_for(Widget.new)
errors.to_a
#=> [
  { type: 'wrong_name', data: {}, path: [:name], message: nil },
  { type: 'wrong_address', data: {}, path: [:manufacturer, :factory, :address], message: nil }
]

contract.does_not_match?(Widget.new) #=> false
errors = contract.negated_errors_for(Widget.new)
errors.to_a
#=> [
  { type: 'is_type', data: { type: Widget }, path: [], message: nil }
]

With An Object That Matches All Of The Property Constraints

factory      = Factory.new('123 Example Street')
manufacturer = Manufacturer.new(factory)
widget       = Widget.new('Self-sealing Stem Bolt', manufacturer)
contract.matches?(widget)        #=> true
contract.errors_for(widget).to_a #=> []

contract.does_not_match?(widget) #=> true
errors = contract.negated_errors_for(widget)
errors.to_a
#=> [
  { type: 'is_type', data: { type: Widget }, path: [], message: nil },
  { type: 'right_name', data: {}, path: [:name], message: nil },
  { type: 'right_address', data: {}, path: [:manufacturer, :factory, :address], message: nil }
]

Defining A Custom Contract

user_contract = Stannum::Contract.new do
  # Sanity constraints are evaluated first, and if a sanity constraint
  # fails, the contract will immediately halt.
  constraint Stannum::Constraints::Type.new(User), sanity: true

  # You can also define a constraint using a block.
  constraint(type: 'example.is_not_user') do |user|
    user.role == 'user'
  end

  # You can define a constraint on a property of the object.
  property :name, Stannum::Constraints::Presence.new
end

See Also:

Defined Under Namespace

Classes: Builder

Constant Summary

Constants inherited from Stannum::Constraints::Base

Stannum::Constraints::Base::NEGATED_TYPE, Stannum::Constraints::Base::TYPE

Instance Attribute Summary

Attributes inherited from Stannum::Constraints::Base

#options

Instance Method Summary collapse

Methods inherited from Stannum::Contracts::Base

#==, #concat, #does_not_match?, #each_constraint, #each_pair, #errors_for, #initialize, #match, #matches?, #negated_errors_for, #negated_match

Methods inherited from Stannum::Constraints::Base

#==, #clone, #does_not_match?, #dup, #errors_for, #initialize, #match, #matches?, #message, #negated_errors_for, #negated_match, #negated_message, #negated_type, #type, #with_options

Constructor Details

This class inherits a constructor from Stannum::Contracts::Base

Instance Method Details

#add_constraint(constraint, property: nil, sanity: false, **options) ⇒ self

Adds a constraint to the contract.

When the contract is matched with an object, the constraint will be evaluated with the object and the errors updated accordingly.

If the :property option is set, this defines a property constraint. See #add_property_constraint for more information.

Parameters:

  • constraint (Stannum::Constraints::Base)

    The constraint to add.

  • sanity (true, false) (defaults to: false)

    Marks the constraint as a sanity constraint, which is always matched first and will always short-circuit on a failed match.

  • options (Hash<Symbol, Object>)

    Options for the constraint. These can be used by subclasses to define the value and error mappings for the constraint.

  • property (String, Symbol, Array<String, Symbol>, nil) (defaults to: nil)

    The property to match.

Returns:

  • (self)

    the contract.

See Also:



139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/stannum/contract.rb', line 139

def add_constraint(constraint, property: nil, sanity: false, **options)
  validate_constraint(constraint)
  validate_property(property: property, **options)

  @constraints << Stannum::Contracts::Definition.new(
    constraint: constraint,
    contract:   self,
    options:    options.merge(property: property, sanity: sanity)
  )

  self
end

#add_property_constraint(property, constraint, sanity: false, **options) ⇒ self

Adds a property constraint to the contract.

When the contract is called, the contract will find the value of that property for the given object. If the property is an array, the contract will recursively retrieve each property.

A property of nil will match against the given object itself, rather than one of its properties.

If the value does not match the constraint, then the error from the constraint will be added in an error namespace matching the constraint. For example, a property of :name will add the error message to errors.dig(:name), while a property of [:manufacturer, :address, :street] will add the error message to errors.dig(:manufacturer, :address, :street).

Parameters:

  • property (String, Symbol, Array<String, Symbol>, nil)

    The property to match.

  • constraint (Stannum::Constraints::Base)

    The constraint to add.

  • sanity (true, false) (defaults to: false)

    Marks the constraint as a sanity constraint, which is always matched first and will always short-circuit on a failed match.

  • options (Hash<Symbol, Object>)

    Options for the constraint. These can be used by subclasses to define the value and error mappings for the constraint.

Returns:

  • (self)

    the contract.

See Also:



181
182
183
# File 'lib/stannum/contract.rb', line 181

def add_property_constraint(property, constraint, sanity: false, **options)
  add_constraint(constraint, property: property, sanity: sanity, **options)
end