Toolchain
Collection of Ruby light-weight modules that enhance plain Ruby classes with zero runtime dependencies.
Available modules
- Attributes
- For defining rich attributes without a data store mapper like ActiveRecord
- Useful for applying principles such as the Single Responsibility Principe (SRP)
- Validations
- Inspired by
ActiveModel::Validations
- Supports nested validations for Hash data types
- Useful for applying principles such as the Single Responsibility Principe (SRP)
- Lightweight (~250 LOC) instead of
ActiveModel::Validations
(~1600 LOC) - Note: This isn't a clone. Don't expect all of the features in
ActiveModel::Validations
to be available here. Part of the public api behaves in a similar fashion, there are however also a bunch of subtle differences.
- Inspired by
Installation
In Gemfile
:
gem "toolchain"
Or, if you wish to only include certain modules:
In Gemfile
:
gem "toolchain", require: false
Then from anywhere in your program/app:
require "toolchain/attributes"
require "toolchain/validations"
Modules
Below follows a list of modules.
Toolchain::Attributes
The Toolchain::Attributes
module provides your class with the attribute
class method which you can use to define rich attributes. Think of it as attr_accessor
but with type-definitions, optional defaults and a few convenience methods.
require "toolchain/attributes"
class Person
include Toolchain::Attributes
attribute :name, String
attribute :cash, Integer
attribute :birthdate, DateTime
attribute :pocket, Array
attribute :misc, Hash
attribute :winning, Boolean, true
end
Defined attributes (keys)
You can get a list of defined attributes usind the Person.keys
method.
All attributes/values as a Hash
You can access a Hash of attributes by calling the person.attributes
instance method. By default this Hash returns all attributes with Symbol keys (including nested Hashes stored in Hash-type attributes).
Hash-type attributes automatically convert String-type keys to Symbol-type keys when setting a new Hash.
(Global) Configuration
You can configure whether you want to have all your attribute keys as String- or Symbol type like so:
Toolchain::Attributes::Configuration.configure do |config|
config.hash_transformation = :stringify_keys # defaults to :symbolize_keys
end
Mass-assignment
You can mass-assign attributes using the person.attributes
instance method. Note that any attributes you mass-assign that you haven't defined on the model are simply ignored and will not be set. Defined attributes automatically act as a whitelist for what can and cannot be mass-assigned.
Toolchain::Validations
The Toolchain::Validations
module provides your class with the validates
and validate
class methods which you can use to validate attributes. Note that this DOES NOT require Toolchain::Attributes
, any instance method that returns a value can be validated.
require "toolchain/validations"
class Person
include Toolchain::Validations
validates :name, presence: true
validates :email, email: { message: "isn't valid" }
validate :domain_validation
attr_accessor :name
attr_accessor :domain
def email; @email end
def email=(value); @email = value end
private
def domain_validation
if true # host validation logic here
errors.add(:domain, "couldn't connect")
end
end
end
Nested Validations
While Toolchain::Validations
mirrors ActiveModel::Validations
in most cases for the sake of consistency, there is an important diference when defining a validation using the validates
method. In Toolchain::Validation
, this creates a nested (Hash) validation:
require "toolchain/validations"
class Person
include Toolchain::Attributes # optional
include Toolchain::Validations
attribute :info, Hash, {}
validates :info, :phone, presence: true
end
Person.new.tap do |person|
person.validate
person.errors[:info][:phone] # => ["can't be blank"]
end
This feature isn't limited to single-level validation. You can have an infinitely deep nested Hash and validate each known attribute on it and it'll properly map the attributes/errors 1:1.
This is useful when you keep a Hash of serialized data in a database, but you still want a hassle-free way of validating it's attributes that doesn't require you to create custom validators and re-invent the wheel just for nested attributes.
Class-level Validators
Similar to ActiveModel::Validations
you can create your own class-level validations.
require "toolchain/validations"
class Person
include Toolchain::Attributes # optional
include Toolchain::Validations
attribute :name, String
attribute :info, Hash, {}
validate :name_validation
validate :info_phone_validation
private
def name_validation
if true # name validation logic here
errors.add(:name, "is invalid")
end
end
def info_validation
if true # info[:phone] validation logic here
errors.add(:info, :phone, "not a number")
end
end
end
Person.new.tap do |person|
person.validate
person.errors[:info][:phone] # => ["can't be blank"]
end
Custom (re-usable) Validators
To define a validator, simply create and load a file like this:
module Toolchain::Validations::Validators
class MyValidator < Base
def validate
errors.add(key_path, message || "is invalid") if invalid?
end
private
def invalid?
# logic that determines that this value is invalid
#
# list of instance methods to work with:
#
# - object
# - errors
# - key_path
# - data
# - message
end
end
end
object
A reference to the object that contains the attribute which is being validated.
errors
A reference to the errors object on the object
. Write potential validation errors to it.
key_path
An array of one or more keys. Single key means it's a root-level attribute validation. If this includes two or more keys, it means it's a nested validation on a Hash-type attribute.
You generally don't need to worry about this. Just pass it in to errors.add
's first argument and it'll take care of properly assigning the error message at the right level.
data
This contains the options data that was passed in to the validates
method.
For example:
validates :cash, format: {
with: /^\d+$/,
message: "not a valid cash format"
}
data[:with] # /^\d+$/
data[:message] # "not a valid cash format"
This can optionally be used in your validations.
message
If data[:message]
is set (see data
example), the message
method will be contain it. Else, message
will return nil
, in which case you'll to use a default error message.
Contributing
- Fork it ( http://github.com/
/toolchain/fork ) - Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request