Keep your Routes, Controllers and Models thin with Plain Old Ruby Objects (PORO).
Add this line to your Gemfile:
gem 'gourami'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install gourami
- Define attributes (inputs & outputs)
- Validate input
- Perform an action
class TypicalForm < Gourami::Form
attribute(:typical_attribute)
def validate
# Define your validation rules here
end
def perform
# Perform your action rules here
end
end
def new
@form = CreateFishBowl.new
end
def create
@form = CreateFishBowl.new(fish_bowl_params)
if @form.valid?
@form.perform
redirect_to @form.record
else
render "new"
end
end
class CreateFishBowl < Gourami::Form
record(:fish_bowl)
attribute(:width, type: :integer)
attribute(:height, type: :integer)
attribute(:liters, type: :float)
attribute(:name, type: :string)
attribute(:filter_included, type: :boolean, default: false)
def validate
validate_presence(:width)
validate_range(:width, min: 50, max: 1000)
validate_presence(:height)
validate_range(:height, min: 50, max: 1000)
validate_presence(:liters)
validate_range(:liters, min: 5, max: 200)
validate_presence(:name)
validate_uniqueness(:name) do |name|
FishBowl.where(name: name).empty?
end
end
def perform
self.fish_bowl = FishBowl.create(attributes)
end
end
def edit
fish_bowl = FishBowl.find(params[:id])
@form = UpdateFishBowl.new_from_record(fish_bowl)
end
def update
@form = UpdateFishBowl.new(fish_bowl_params)
if @form.valid?
@form.perform
redirect_to @form.record
else
render "edit"
end
end
class UpdateFishBowl < Gourami::Form
record(:fish_bowl)
attribute(:width, type: :integer)
attribute(:height, type: :integer)
attribute(:liters, type: :float)
attribute(:name, type: :string)
attribute(:filter_included, type: :boolean, default: false)
def self.new_from_record(fish_bowl)
new(fish_bowl.attributes.merge(fish_bowl: fish_bowl))
end
def validate
validate_presence(:width)
validate_range(:width, min: 50, max: 1000)
validate_presence(:height)
validate_range(:height, min: 50, max: 1000)
validate_presence(:liters)
validate_range(:liters, min: 5, max: 200)
validate_presence(:name)
validate_uniqueness(:name) do |name|
FishBowl.where(name: name).empty?
end
end
def perform
fish_bowl.update(attributes)
end
end
class UpdateFishBowl < CreateFishBowl
# All attributes and validations inherited from CreateFishBowl.
def self.new_from_record(fish_bowl)
new(fish_bowl.attributes.merge(fish_bowl: fish_bowl))
end
def perform
fish_bowl.update(attributes)
end
end
The following examples will result in all :string
attributes getting the options :strip
and :upcase
set to true
.
Set global defaults:
Gourami::Form.set_default_attribute_options(:string, upcase: true)
# Make sure to define CreateFishBowl and other forms AFTER setting default options.
class CreateFishBowl < Gourami::Form
attribute(:name, type: :string)
end
form = CreateFishBowl.new(name: "Snake Gyllenhaal")
form.name # => "SNAKE GYLLENHAAL"
Instead of global defaults, you can also apply defaults to certain form classes.
Just as attributes
are inherited by subclasses, so are default_attribute_options
.
Set local defaults:
class ScreamingForm < Gourami::Form
set_default_attribute_options(:string, upcase: true)
end
class CreateScreamingFish < ScreamingForm
attribute(:name, type: :string)
end
class UpdateScreamingFish < CreateScreamingFish; end
create_form = CreateScreamingFish.new(name: "Snake Gyllenhaal")
create_form.name # => "SNAKE GYLLENHAAL"
update_form = UpdateScreamingFish.new(name: "Snake Gyllenhaal")
update_form.name # => "SNAKE GYLLENHAAL"
# Other Gourami::Forms are unaffected
class RegularForm < Gourami::Form
attribute(:name, type: :string)
end
regular_form = RegularForm.new(name: "Snake Gyllenhaal")
regular_form.name # => "Snake Gyllenhaal"
Check to see if an attribute is being changed:
class UpdateUserEmail < Gourami::Form
include Gourami::Extensions::Changes
record(:user)
attribute(:email, :type => :string, :watch_changes => true)
def perform
user.update(attributes)
do_something_like_send_confirmation_email(email) if changes?(:email)
end
end
This is the equivalent behavior when you set :watch_changes => true
attribute(:email, :watch_changes => ->(new_value) { new_value != user.email })
Your logic to check for changes can be as sophisticated as you want.
class UpdatePageAuthorizedUsers < Gourami::Form
include Gourami::Extensions::Changes
record(:page)
attribute(:authorized_users,
:type => :array,
:watch_changes => ->(new_value) { new_value.sort.uniq != page.authorized_users.sort.uniq })
def perform
page.update(attributes)
do_something_like_notify_authorization_libraries(authorized_users) if changes?(:authorized_users)
end
end
You can also keep track of side effects due to changes by using did_change
.
class UpdatePageWidgets < Gourami::Form
include Gourami::Extensions::Changes
record(:page)
attribute(:widgets,
:type => :array,
:element_type => :string,
:watch_changes => ->(new_value) {
did_change(:pro_widget, new_value.include?("pro"))
new_value.sort.uniq != page.widgets.sort.uniq
})
def validate
append_error(:widgets, :unauthorized) if changes?(:pro_widget) && !current_user_has_pro_account?
end
end
After checking out the repo, run bin/setup
to install dependencies. Then, run rake test
to run the tests, or rake test:watch
to automatically rerun the tests when you make code changes. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
.
To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
To add another gem owner to gourami gem gem owner --add [email protected] gourami
Bug reports and pull requests are welcome on GitHub at https://github.com/Vydia/gourami. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
The gem is available as open source under the terms of the MIT License.