Soulless

Rails models without the database (and Rails). Great for implementing the form object pattern.

Build Status Dependency Status Coverage Status Code Climate

Installation

Add this line to your application's Gemfile:

gem 'soulless'

And then execute:

$ bundle

Or install it yourself as:

$ gem install soulless

Usage

Just define a plain-old-ruby-object, include Soulless and get crackin'!

class UserSignupForm
  include Soulless.model

  attribute :name, String
  attribute :email, String
  attribute :password, String

  validates :name, presence: true

  validates :email, presence: true,
                    uniqueness: { model: User }

  validates :password, presence: true,
                       lenght: { is_at_least: 8 }

  private
  def persist!
    # Define what to do when this form is ready to be saved.
  end
end

Processing an Object

Soulless let's you define what happens when your object is ready to be processed.

class UserSignupForm

  ...

  private
  def persist!
    user = User.create!(name: name, email: email, password: password)
    UserMailer.send_activation_code(user).deliver
    user.charge_card!
  end
end

Process your Soulless object by calling save. Just like a Rails model!

form = UserSignupForm.new(name: name, email: email, password: passord)
if form.save
  # Called persist! and all is good!
else
  # Looks like a validation failed. Try again.
end

Validations and Errors

Soulless lets you define your validations and manage your errors just like you did in Rails.

class UserSignupForm

  ...

  validates :name, presence: true

  validates :email, presence: true,
                    uniqueness: { model: User }

  validates :password, presence: true,
                       lenght: { minimum: 8 }

  ...

end

Check to see if your object is valid by calling valid?.

form = UserSignupForm.new(name: name, email: email)
form.valid? # => false

See what errors are popping up using the errors attribute.

form = UserSignupForm.new(name: name, email: email)
form.valid?
form.errors[:password] # => ["is too short (minimum is 8 characters)"]

Uniqueness Validations

If you're using Soulless in Rails it's even possible to validate uniqueness.

class UserSignupForm

  ...

  validates :primary_email, presence: true,
                            uniqueness: { model: User, attribute: :email }

  ...

end

Just let the validator know what ActiveRecord model to use when performing the validation using the model option.

If your Soulless object attribute doesn't match up to the ActiveRecord model attribute just map it using the attribute option.

has_one and has_many Associations

When you need associations use has_one and has_many. Look familiar?

class Person
  include Soulless.model

  attribute :name, String

  validates :name, presence: true

  has_one :spouse do
    attribute :name, String
  end

  has_many :friends do
    attribute :name, String
  end
end

You can set has_one and has_many attributes by setting their values hashes and hash arrays.

person = Person.new(name: 'Anthony')
person.spouse = { name: 'Megan' }
person.friends = [{ name: 'Yaw' }, { name: 'Biff' }]

It's also possible for an association to inherit from a parent class and then extend functionality.

class Person
  include Soulless.model

  attribute :name, String

  validates :name, presence: true

  has_one :spouse, Person do # inherits 'name' and validation from Person
    attribute :anniversary, DateTime

    validates :anniversary, presence: true
  end

  has_many :friends, Person # just inherit from Person, don't extend
end

Your association has access to it's parent object as well.

class Person
  include Soulless.model

  attribute :name, String

  validates :name, presence: true

  has_one :children do
    attribute :name, String, default: lambda { "#{parent.name} Jr." }
  end
end

When you need to make sure an association is valid before processing the object use validates_associated.

class Person

  ...

  has_one :spouse do
    attribute :name, String

    validates :name, presence: true
  end

  validates_associated :spouse

  ...

end

person = Person.new(name: 'Anthony')
person.spouse = { name: nil }
person.valid? # => false
person.errors[:spouse] # => ["is invalid"]
person.spouse.errors[:name] # => ["can't be blank"]

Dirty Attributes

Dirty attribute allow you to track changes to a Soulless object before it's saved.

person = Person.name(name: "Anthony", spouse: { name: "Mary Jane Watson" })
person.name = 'Peter Parker'
person.changed? # => true
person.changed # => ["name"]
person.changes # => { name: ["Anthony", "Peter Parker"] }
person.name_changed? # => true
person.name_was # => "Anthony"
person.name_change # => ["Anthony", "Peter Parker"]

Works on has_one and has_many too.

person.spouse.name = 'Gwen Stacy'
person.spouse.changed? # => true
person.spouse.changed # => ["name"]
person.spouse.changes # => { name: ["Mary Jane Watson", "Gwen Stacy"] }
person.spouse.name_changed? # => true
person.spouse.name_was # => "Mary Jane Watson"
person.spouse.name_change # => ["Mary Jane Watson", "Gwen Stacy"]
person.changed? # => false

I18n

Define locales similar to how you would define them in Rails.

en:
  soulless:
    errors:
      models:
        person:
          name:
            blank: "there's nothing here"

For attributes defined as has_one and has_many associations use the enclosing class as the locale key's namespace.

en:
  soulless:
    errors:
      models:
        person/spouse:
          name:
            blank: "there's nothing here"

Contributing

  1. Fork it
  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

Credits

Sticksnleaves

Soulless is maintained and funded by Sticksnleaves

Thanks to all of our contributors