Project

gourami

0.01
Low commit activity in last 3 years
A long-lived project that still receives updates
Create Plain Old Ruby Objects that take attributes, validate them, and perform an action.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 1.1.0
~> 5.0
~> 0.10
~> 10.0
 Project Readme

Gourami

Github Actions CI

Keep your Routes, Controllers and Models thin with Plain Old Ruby Objects (PORO).

Installation

Add this line to your Gemfile:

gem 'gourami'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install gourami

Usage

A Typical Gourami::Form will

  • 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

Your Rails 5 ActionController for the New/Create action:

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

Example of a form that Creates a record

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

Your Rails 5 ActionController for the Edit/Update action:

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

Example of a form that Updates a record

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

Or inherit instead of duplicating the attributes and validations

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

Configure default attribute options

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"

Extensions / Plugins

Gourami::Extensions::Changes

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
You can implement custom logic to determine if an attribute is changing

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

Development

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 john.smith@example.com gourami

Contributing

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.

License

The gem is available as open source under the terms of the MIT License.