Assertion
Immutable assertions and validations for PORO.
Synopsis
The primary goal of the gem is to make assertions about
No ActiveSupport
, no mutation of any instances.
Basic Usage
Define an assertion by inheriting it from the Assertion::Base
class with attributes to which it should be applied.
Then implement the method check
to describe if the assertion is truthy or falsey.
You can do it either in the classic style:
class IsAdult < Assertion::Base
attribute :age, :name
def check
age.to_i >= 18
end
end
or with more verbose builder:
IsAdult = Assertion.about :age, :name do
age.to_i >= 18
end
Define translations to describe both the truthy and falsey states of the assertion.
All the attributes are available in translations (that's why we declared the name
as an attribute):
# config/locales/en.yml
---
en:
assertion:
is_adult:
truthy: "%{name} is already an adult (age %{age})"
falsey: "%{name} is a child yet (age %{age})"
Check a state of an assertion for some argument(s), using class method []
:
john = { name: 'John', age: 10, gender: :male }
state = IsAdult[john]
# => #<Assertion::State @state=false, @messages=["John is a child yet (age 10)"]>
The state supports valid?
, invalid?
, messages
and validate!
methods:
state.valid? # => false
state.invalid? # => true
state. # => ["John is a child yet (age 10)"]
state.validate! # => #<Assertion::InvalidError @messages=["John is a child yet (age 10)"]>
Inversion
Use the .not
class method to negate the assertion:
jack = { name: 'Jack', age: 21, gender: :male }
IsAdult.not[jack]
# => #<Assertion::State @state=false, @messages=["Jack is already an adult (age 21)"]>
Composition
You can compose assertion states (results):
IsMale = Assertion.about :name, :gender do
gender == :male
end
# config/locales/en.yml
---
en:
assertion:
is_male:
truthy: "%{name} is a male"
falsey: "%{name} is a female"
Use method &
(or its aliases +
or >>
) to compose assertion states:
jane = { name: 'Jane', age: 16, gender: :female }
state = IsAdult[jane] & IsMale[jane]
# => #<Assertion::State @state=false, @messages=["Jane is a child yet (age 16)", "Jane is a female"]>
Guards
The guard class is a lean wrapper around the state of its object.
It defines the #state
for the object and checks if the state is valid:
class VoterOnly < Assertion::Guard
alias_method :user, :object
def state
IsAdult[user.attributes] & IsCitizen[user.attributes]
end
end
Or using the verbose builder Assertion.guards
:
VoterOnly = Assertion.guards :user do
IsAdult[user.attributes] & IsCitizen[user.attributes]
end
When the guard is called for some object, its calls #validate!
and then returns the source object. That simple.
jack = OpenStruct.new(name: "Jack", age: 15, citizen: true)
john = OpenStruct.new(name: "John", age: 34, citizen: true)
voter = VoterOnly[jack]
# => #<Assertion::InvalidError @messages=["Jack is a child yet (age 15)"]
voter = VoterOnly[john]
# => #<OpenStruct @name="John", @age=34>
Naming Convention
This is not necessary, but for verbosity you could follow the rules:
- use the prefixex
Is
(Are
) for assertions (likeIsAdult
,AreConsistent
etc.) - use the suffix
Only
for guards (likeAdultOnly
)
Edge Cases
You cannot define attributes with names already defined as istance methods:
IsAdult = Assertion.about :check
# => #<Assertion::NameError @message="Wrong name(s) for attribute(s): check">
AdultOnly = Assertion.guards :state
# => #<Assertion::NameError @message="Wrong name(s) for attribute(s): state">
Installation
Add this line to your application's Gemfile:
# Gemfile
gem "assertion"
Then execute:
bundle
Or add it manually:
gem install assertion
Compatibility
Tested under rubies compatible to MRI 1.9+.
Uses RSpec 3.0+ for testing and hexx-suit for dev/test tools collection.
Contributing
- Read the STYLEGUIDE
- Fork the project
- Create your feature branch (
git checkout -b my-new-feature
) - Add tests for it (please, use mutant to verify the coverage!)
- Commit your changes (
git commit -am '[UPDATE] Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request
License
See the MIT LICENSE.