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.

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

  1. Fork it ( http://github.com//toolchain/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request