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, inherit from Soulless::Model
and get crackin'!
class UserSignupForm < 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 }
end
Validations and Errors
Soulless lets you define your validations and manage your errors just like you did in Rails.
class UserSignupForm < Soulless::Model
...
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 < Soulless::Model
...
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.
Callbacks
Soulless supports the validation callback. You can use this callback as you would on a Rails model.
class Person < Soulless::Model
attribute :name, String
validates :name, presence: true
before_validation :change_name_to_bart_simpson
private
def change_name_to_bart_simpson
self.name = 'Bart Simpson'
end
end
person = Person.new(name: 'Anthony')
person.valid?
person.name # => "Bart Simpson"
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"]
Inheritance
One of the biggest pitfalls of the form object pattern is duplication of code. It's not uncommon for a form object to define attributes and validations that are identical to the model it represets.
To get rid of this annoying issue Soulless implements the #inherit_from(klass, options = {})
method. This method will allow a Soulless model to inherit attributes and validations from any Rails model, Soulless model or Virtus object.
class User < ActiveRecord::Base
validates :name, presence: true
validates :email, presence: true,
uniqueness: { case_insensitive: true }
end
class UserSignupForm < Soulless::Model
inherit_from(User)
end
The UserSignupForm
has automatically been defined with the name
and email
attributes and validations.
UserSignupForm.attributes # => name and email attributes
form = UserSignupForm.new
form.valid? # => false
form.errors.messages # => { name: ["can't be blank"], email: ["can't be blank"] }
If your model is using the uniqueness validator it will automatically convert it to the uniqueness validator provided by Soulless.
The #inherit_from(klass, options = {})
method also allows you to provide options for managing inherited attributes.
If you don't want to inherit the email
attribute define it using the exclude
option.
class UserSignupForm < Soulless::Model
inherit_from(User, exclude: :email)
end
UserSignupForm.attributes # => email will not be inherited
form = UserSignupForm.new
form.valid? # => false
form.errors.messages # => { name: ["can't be blank"] }
You can also flip it around if you only want the name
attribute by using the only
option.
class UserSignupForm < Soulless::Model
inherit_from(User, only: :name)
end
UserSignupForm.attributes # => email will not be inherited
form = UserSignupForm.new
form.valid? # => false
form.errors.messages # => { name: ["can't be blank"] }
Available Options
only
- Only inherit the attributes and validations for the provided attributes. Any attributes not specified will be ignored. Accepts strings, symbols and an array of strings or symbols.
exclude
- Don't inherit the attributes and validations for the provided attributes. Accepts strings, symbols and an array of strings or symbols.
skip_validators
- Only inherit attributes. Don't inherit any validators. Accepts a boolean.
use_database_default
- Use the value of the default
migration option as the default value for an attribute. Accepts either a boolean (for all attributes), a string or symbol for a single attribute or an array of strings and symbols for multiple attributes.
additional_attributes
- Used to specify attributes that cannot automatically be added to the form model. These are generally attributes that have been specified using attr_accessor
. Accepts a string, symbol or an array of strings and symbols for multiple attributes.
validate_attribute_on
- By default any validation that specifies an :on
option will not be inherited. This option will allow you to inherit a validator that uses the :on
with a specific value. Example usage: validate_password_on: :create
. Accepts a string or symbol. This option will accept any value that the Rails :on
validator option can accept.
Serialization
Soulless automatically handles serializing and deserializing Soulless models using the standard ActiveRecord serialization methods. Serialization currently only handles JSON and array data types.
class Profile < Soulless::Model
attribute :first_name, String
attribute :last_name, String
...
end
class User < ActiveRecord::Base
serialize :profile, Profile
...
end
user = User.new
user.profile = { first_name: 'Anthony', last_name: 'Smith' }
user.profile # => Profile(first_name: 'Anthony', last_name: 'Smith')
If you want to implement your own serialization/deserialization process just
implement your own self.load(value)
and self.dump(value)
methods on your
model.
I18n
Define locales similar to how you would define them in Rails.
en:
soulless:
errors:
models:
person:
attributes:
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