{"id":20714419,"url":"https://github.com/vydia/gourami","last_synced_at":"2025-04-23T08:51:13.847Z","repository":{"id":54171451,"uuid":"76314218","full_name":"Vydia/gourami","owner":"Vydia","description":"Thinner Routes, Controllers, Models. Simple \u0026 extensible.","archived":false,"fork":false,"pushed_at":"2023-05-09T18:27:47.000Z","size":80,"stargazers_count":12,"open_issues_count":1,"forks_count":2,"subscribers_count":14,"default_branch":"master","last_synced_at":"2025-04-13T10:53:52.632Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Vydia.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-12-13T02:19:03.000Z","updated_at":"2024-09-30T05:54:33.000Z","dependencies_parsed_at":"2024-11-17T04:02:35.836Z","dependency_job_id":null,"html_url":"https://github.com/Vydia/gourami","commit_stats":{"total_commits":71,"total_committers":6,"mean_commits":"11.833333333333334","dds":"0.23943661971830987","last_synced_commit":"ed910bd436b4237159e7336a39c790e7a3d3be86"},"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Vydia%2Fgourami","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Vydia%2Fgourami/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Vydia%2Fgourami/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Vydia%2Fgourami/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Vydia","download_url":"https://codeload.github.com/Vydia/gourami/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250403069,"owners_count":21424771,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-17T02:31:48.284Z","updated_at":"2025-04-23T08:51:13.842Z","avatar_url":"https://github.com/Vydia.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Gourami\n\n![Github Actions CI](https://github.com/Vydia/gourami/workflows/ci/badge.svg)\n\nKeep your Routes, Controllers and Models thin with Plain Old Ruby Objects (PORO).\n\n## Installation\n\nAdd this line to your Gemfile:\n\n```ruby\ngem 'gourami'\n```\n\nAnd then execute:\n\n    $ bundle install\n\nOr install it yourself as:\n\n    $ gem install gourami\n\n## Usage\n\n### A Typical `Gourami::Form` will\n\n - Define attributes (inputs \u0026 outputs)\n - Validate input\n - Perform an action\n\n```ruby\nclass TypicalForm \u003c Gourami::Form\n\n  attribute(:typical_attribute)\n\n  def validate\n    # Define your validation rules here\n  end\n\n  def perform\n    # Perform your action rules here\n  end\n\nend\n```\n\n### Your Rails 5 ActionController for the New/Create action:\n\n```ruby\ndef new\n  @form = CreateFishBowl.new\nend\n\ndef create\n  @form = CreateFishBowl.new(fish_bowl_params)\n\n  if @form.valid?\n    @form.perform\n    redirect_to @form.record\n  else\n    render \"new\"\n  end\nend\n```\n\n### Example of a form that Creates a record\n\n```ruby\nclass CreateFishBowl \u003c Gourami::Form\n\n  record(:fish_bowl)\n  attribute(:width, type: :integer)\n  attribute(:height, type: :integer)\n  attribute(:liters, type: :float)\n  attribute(:name, type: :string)\n  attribute(:filter_included, type: :boolean, default: false)\n\n  def validate\n    validate_presence(:width)\n    validate_range(:width, min: 50, max: 1000)\n\n    validate_presence(:height)\n    validate_range(:height, min: 50, max: 1000)\n\n    validate_presence(:liters)\n    validate_range(:liters, min: 5, max: 200)\n\n    validate_presence(:name)\n    validate_uniqueness(:name) do |name|\n      FishBowl.where(name: name).empty?\n    end\n  end\n\n  def perform\n    self.fish_bowl = FishBowl.create(attributes)\n  end\n\nend\n```\n\n### Your Rails 5 ActionController for the Edit/Update action:\n\n```ruby\ndef edit\n  fish_bowl = FishBowl.find(params[:id])\n  @form = UpdateFishBowl.new_from_record(fish_bowl)\nend\n\ndef update\n  @form = UpdateFishBowl.new(fish_bowl_params)\n\n  if @form.valid?\n    @form.perform\n    redirect_to @form.record\n  else\n    render \"edit\"\n  end\nend\n```\n\n### Example of a form that Updates a record\n\n```ruby\nclass UpdateFishBowl \u003c Gourami::Form\n\n  record(:fish_bowl)\n  attribute(:width, type: :integer)\n  attribute(:height, type: :integer)\n  attribute(:liters, type: :float)\n  attribute(:name, type: :string)\n  attribute(:filter_included, type: :boolean, default: false)\n\n  def self.new_from_record(fish_bowl)\n    new(fish_bowl.attributes.merge(fish_bowl: fish_bowl))\n  end\n\n  def validate\n    validate_presence(:width)\n    validate_range(:width, min: 50, max: 1000)\n\n    validate_presence(:height)\n    validate_range(:height, min: 50, max: 1000)\n\n    validate_presence(:liters)\n    validate_range(:liters, min: 5, max: 200)\n\n    validate_presence(:name)\n    validate_uniqueness(:name) do |name|\n      FishBowl.where(name: name).empty?\n    end\n  end\n\n  def perform\n    fish_bowl.update(attributes)\n  end\n\nend\n```\n\n#### Or inherit instead of duplicating the attributes and validations\n\n```ruby\nclass UpdateFishBowl \u003c CreateFishBowl\n\n  # All attributes and validations inherited from CreateFishBowl.\n\n  def self.new_from_record(fish_bowl)\n    new(fish_bowl.attributes.merge(fish_bowl: fish_bowl))\n  end\n\n  def perform\n    fish_bowl.update(attributes)\n  end\n\nend\n```\n\n#### Configure default attribute options\n\nThe following examples will result in all `:string` attributes getting the options `:strip` and `:upcase` set to `true`.\n\nSet global defaults:\n\n```ruby\nGourami::Form.set_default_attribute_options(:string, upcase: true)\n\n# Make sure to define CreateFishBowl and other forms AFTER setting default options.\nclass CreateFishBowl \u003c Gourami::Form\n  attribute(:name, type: :string)\nend\n\nform = CreateFishBowl.new(name: \"Snake Gyllenhaal\")\nform.name # =\u003e \"SNAKE GYLLENHAAL\"\n```\n\nInstead of global defaults, you can also apply defaults to certain form classes.\n\nJust as `attributes` are inherited by subclasses, so are `default_attribute_options`.\n\nSet local defaults:\n\n```ruby\nclass ScreamingForm \u003c Gourami::Form\n  set_default_attribute_options(:string, upcase: true)\nend\n\nclass CreateScreamingFish \u003c ScreamingForm\n  attribute(:name, type: :string)\nend\n\nclass UpdateScreamingFish \u003c CreateScreamingFish; end\n\ncreate_form = CreateScreamingFish.new(name: \"Snake Gyllenhaal\")\ncreate_form.name # =\u003e \"SNAKE GYLLENHAAL\"\n\nupdate_form = UpdateScreamingFish.new(name: \"Snake Gyllenhaal\")\nupdate_form.name # =\u003e \"SNAKE GYLLENHAAL\"\n\n# Other Gourami::Forms are unaffected\nclass RegularForm \u003c Gourami::Form\n  attribute(:name, type: :string)\nend\n\nregular_form = RegularForm.new(name: \"Snake Gyllenhaal\")\nregular_form.name # =\u003e \"Snake Gyllenhaal\"\n```\n\n#### Extensions / Plugins\n\n##### Gourami::Extensions::Changes\n\nCheck to see if an attribute is being changed:\n\n```ruby\nclass UpdateUserEmail \u003c Gourami::Form\n\n  include Gourami::Extensions::Changes\n\n  record(:user)\n  attribute(:email, :type =\u003e :string, :watch_changes =\u003e true)\n\n  def perform\n    user.update(attributes)\n\n    do_something_like_send_confirmation_email(email) if changes?(:email)\n  end\n\nend\n```\n\n###### You can implement custom logic to determine if an attribute is changing\n\nThis is the equivalent behavior when you set `:watch_changes =\u003e true`\n\n```ruby\nattribute(:email, :watch_changes =\u003e -\u003e(new_value) { new_value != user.email })\n```\n\nYour logic to check for changes can be as sophisticated as you want.\n\n```ruby\nclass UpdatePageAuthorizedUsers \u003c Gourami::Form\n\n  include Gourami::Extensions::Changes\n\n  record(:page)\n  attribute(:authorized_users,\n    :type =\u003e :array,\n    :watch_changes =\u003e -\u003e(new_value) { new_value.sort.uniq != page.authorized_users.sort.uniq })\n\n  def perform\n    page.update(attributes)\n\n    do_something_like_notify_authorization_libraries(authorized_users) if changes?(:authorized_users)\n  end\n\nend\n```\n\nYou can also keep track of side effects due to changes by using `did_change`.\n\n```ruby\nclass UpdatePageWidgets \u003c Gourami::Form\n\n  include Gourami::Extensions::Changes\n\n  record(:page)\n  attribute(:widgets,\n    :type =\u003e :array,\n    :element_type =\u003e :string,\n    :watch_changes =\u003e -\u003e(new_value) {\n      did_change(:pro_widget, new_value.include?(\"pro\"))\n      new_value.sort.uniq != page.widgets.sort.uniq\n    })\n\n  def validate\n    append_error(:widgets, :unauthorized) if changes?(:pro_widget) \u0026\u0026 !current_user_has_pro_account?\n  end\n\nend\n```\n\n## Development\n\nAfter 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.\n\nTo install this gem onto your local machine, run `bundle exec rake install`.\n\nTo 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](https://rubygems.org).\n\nTo add another gem owner to gourami gem `gem owner --add john.smith@example.com gourami`\n\n## Contributing\n\nBug 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](http://contributor-covenant.org) code of conduct.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvydia%2Fgourami","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvydia%2Fgourami","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvydia%2Fgourami/lists"}