Soulless
Rails models without the database (and Rails). Great for implementing the form object pattern.
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
- Fork it
- 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
Credits
Soulless is maintained and funded by Sticksnleaves
Thanks to all of our contributors




