class Stannum::Contract
A Contract
defines constraints on an object and its properties.
@example 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])
@example 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 #=> []
@example 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 } ]
@example 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 } ]
@example 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 Stannum::Contracts::Base
.
Public Instance Methods
(see Stannum::Contracts::Base#add_constraint
)
If the :property option is set, this defines a property constraint. See add_property_constraint
for more information.
@param property [String, Symbol, Array<String, Symbol>, nil] The
property to match.
@see add_property_constraint
.
# 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
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).
@param property [String, Symbol, Array<String, Symbol>, nil] The
property to match.
@param constraint [Stannum::Constraints::Base] The constraint to add. @param sanity [true, false] Marks the constraint as a sanity constraint,
which is always matched first and will always short-circuit on a failed match.
@param options [Hash<Symbol, Object>] Options for the constraint. These
can be used by subclasses to define the value and error mappings for the constraint.
@return [self] the contract.
@see add_constraint
.
# 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
Protected Instance Methods
# File lib/stannum/contract.rb, line 187 def map_errors(errors, **options) property_name = options.fetch(:property_name, options[:property]) return errors if property_name.nil? errors.dig(*Array(property_name)) end
# File lib/stannum/contract.rb, line 195 def map_value(actual, **options) property = options[:property] return actual if property.nil? access_nested_property(actual, property) end
Private Instance Methods
# File lib/stannum/contract.rb, line 205 def access_nested_property(object, property) Array(property).reduce(object) { |obj, prop| access_property(obj, prop) } end
# File lib/stannum/contract.rb, line 209 def access_property(object, property) object.send(property) if object.respond_to?(property, true) end
# File lib/stannum/contract.rb, line 213 def valid_property?(property: nil, **_options) if property.is_a?(Array) return false if property.empty? return property.all? { |item| valid_property_name?(item) } end valid_property_name?(property) end
# File lib/stannum/contract.rb, line 223 def valid_property_name?(name) return false unless name.is_a?(String) || name.is_a?(Symbol) !name.empty? end
# File lib/stannum/contract.rb, line 229 def validate_property(**options) return unless validate_property?(**options) return if valid_property?(**options) raise ArgumentError, "invalid property name #{options[:property].inspect}", caller(1..-1) end
# File lib/stannum/contract.rb, line 239 def validate_property?(property: nil, **_options) !property.nil? end