{"id":15663109,"url":"https://github.com/jonathanhefner/garden_variety","last_synced_at":"2025-11-11T18:39:56.329Z","repository":{"id":55025201,"uuid":"112150015","full_name":"jonathanhefner/garden_variety","owner":"jonathanhefner","description":"Delightfully boring Rails controllers","archived":false,"fork":false,"pushed_at":"2023-03-18T17:36:20.000Z","size":123,"stargazers_count":18,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-10-17T04:33:32.901Z","etag":null,"topics":["controllers","rails","ruby","ruby-on-rails"],"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/jonathanhefner.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"MIT-LICENSE","code_of_conduct":null,"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":"2017-11-27T05:08:27.000Z","updated_at":"2024-01-05T23:43:04.000Z","dependencies_parsed_at":"2024-10-23T08:25:55.166Z","dependency_job_id":"151267be-ea0e-4fff-bd90-9f118893c4b0","html_url":"https://github.com/jonathanhefner/garden_variety","commit_stats":{"total_commits":111,"total_committers":2,"mean_commits":55.5,"dds":"0.26126126126126126","last_synced_commit":"4d1999b8befadb89940278d8fe852c8b49c5eb04"},"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/jonathanhefner/garden_variety","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonathanhefner%2Fgarden_variety","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonathanhefner%2Fgarden_variety/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonathanhefner%2Fgarden_variety/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonathanhefner%2Fgarden_variety/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jonathanhefner","download_url":"https://codeload.github.com/jonathanhefner/garden_variety/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonathanhefner%2Fgarden_variety/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":283910127,"owners_count":26915128,"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","status":"online","status_checked_at":"2025-11-11T02:00:06.610Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["controllers","rails","ruby","ruby-on-rails"],"created_at":"2024-10-03T13:35:30.887Z","updated_at":"2025-11-11T18:39:56.312Z","avatar_url":"https://github.com/jonathanhefner.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# garden_variety\n\nDelightfully boring Rails controllers.  One of the superb advantages of\nRuby on Rails is convention over configuration.  Opinionated default\nbehavior can decrease development time and increase application\nrobustness (less custom code == less that can go wrong).  In service of\nthis principle, *garden_variety* provides reasonable default controller\nactions, with care to allow easy override.\n\n*garden_variety* also relies on the [Pundit](https://rubygems.org/gems/pundit)\ngem to isolate authorization concerns.  If you're unfamiliar with\nPundit, see its documentation for an explanation of policy objects and\nhow they help controller actions stay DRY and boring.\n\nAs an example, this controller using `garden_variety`...\n\n```ruby\nclass PostsController \u003c ApplicationController\n  garden_variety\nend\n```\n\n...is equivalent to the following conventional implementation:\n\n```ruby\nclass PostsController \u003c ApplicationController\n\n  def index\n    authorize(self.class.model_class)\n    self.collection = policy_scope(find_collection)\n  end\n\n  def show\n    self.model = authorize(find_model)\n  end\n\n  def new\n    self.model = authorize(new_model)\n    if params.key?(self.class.model_class.model_name.param_key)\n      assign_attributes(model)\n    end\n  end\n\n  def create\n    self.model = assign_attributes(authorize(new_model))\n    if model.save\n      flash[:success] = flash_message(:success)\n      redirect_to model\n    else\n      flash.now[:error] = flash_message(:error)\n      render :new\n    end\n  end\n\n  def edit\n    self.model = authorize(find_model)\n  end\n\n  def update\n    self.model = assign_attributes(authorize(find_model))\n    if model.save\n      flash[:success] = flash_message(:success)\n      redirect_to model\n    else\n      flash.now[:error] = flash_message(:error)\n      render :edit\n    end\n  end\n\n  def destroy\n    self.model = authorize(find_model)\n    if model.destroy\n      flash[:success] = flash_message(:success)\n      redirect_to action: :index\n    else\n      flash.now[:error] = flash_message(:error)\n      render :show\n    end\n  end\n\n  private\n\n  def self.model_class\n    Post\n  end\n\n  def collection\n    @posts\n  end\n\n  def collection=(models)\n    @posts = models\n  end\n\n  def model\n    @post\n  end\n\n  def model=(model)\n    @post = model\n  end\n\n  def find_collection\n    self.class.model_class.all\n  end\n\n  def find_model\n    self.class.model_class.find(params[:id])\n  end\n\n  def new_model\n    self.class.model_class.new\n  end\n\n  def assign_attributes(model)\n    model.assign_attributes(permitted_attributes(model))\n    model\n  end\n\n  def flash_options\n    { model_name: \"Post\" }\n  end\n\n  def flash_message(status)\n    keys = [\n      :\"flash.posts.#{action_name}.#{status}\",\n      :\"flash.posts.#{action_name}.#{status}_html\",\n      :\"flash.#{action_name}.#{status}\",\n      :\"flash.#{action_name}.#{status}_html\",\n      :\"flash.#{status}\",\n      :\"flash.#{status}_html\",\n    ]\n    helpers.translate(keys.first, { default: keys.drop(1), **flash_options })\n  end\n\nend\n```\n\nThe `::model_class` method returns a class corresponding to the\ncontroller name, by default.  That value can be overridden using the\nmatching `::model_class=` setter.  The `model` / `collection` accessor\nmethod definitions are dictated by `::model_class`.  The rest of the\nmethods can be overridden as normal, a la carte.  For a detailed\ndescription of method behavior, see the [API documentation](\nhttps://www.rubydoc.info/gems/garden_variety/).  (Note that the\n`authorize`, `policy_scope`, and `permitted_attributes` methods are\nprovided by Pundit.)\n\n\n## Scaffold generator\n\n*garden_variety* includes a `garden:scaffold` generator similar to the\nRails `scaffold` generator:\n\n```\n$ rails generate garden:scaffold post title:string body:text published:boolean\n    generate  resource\n      invoke  active_record\n      create    db/migrate/19991231235959_create_posts.rb\n      create    app/models/post.rb\n      invoke    test_unit\n      create      test/models/post_test.rb\n      create      test/fixtures/posts.yml\n      invoke  controller\n      create    app/controllers/posts_controller.rb\n      invoke    erb\n      create      app/views/posts\n      invoke    test_unit\n      create      test/controllers/posts_controller_test.rb\n      invoke    helper\n      create      app/helpers/posts_helper.rb\n      invoke      test_unit\n      invoke    assets\n      invoke      coffee\n      create        app/assets/javascripts/posts.coffee\n      invoke      scss\n      create        app/assets/stylesheets/posts.scss\n      invoke  resource_route\n       route    resources :posts\n    generate  erb:scaffold\n       exist  app/views/posts\n      create  app/views/posts/index.html.erb\n      create  app/views/posts/edit.html.erb\n      create  app/views/posts/show.html.erb\n      create  app/views/posts/new.html.erb\n      create  app/views/posts/_form.html.erb\n      insert  app/controllers/posts_controller.rb\n    generate  pundit:policy\n      create  app/policies/post_policy.rb\n      invoke  test_unit\n      create    test/policies/post_policy_test.rb\n```\n\nThe generated controller will contain only a call to the\n`garden_variety` macro.  Also, as you can see from the command output,\nthe *garden_variety* scaffold generator differs from the Rails scaffold\ngenerator in a few small ways:\n\n* No scaffold CSS (i.e. no \"app/assets/stylesheets/scaffolds.scss\").\n* No jbuilder templates.  Only HTML templates are generated.\n* `rails generate pundit:policy` is invoked for the specified model.\n\nAdditionally, if you are using the [talent_scout] gem, the scaffold\ngenerator will invoke `rails generate talent_scout:search` for the\nspecified model.  This behavior can be disabled with the\n`--skip-talent-scout` option.  For more information about integrating\nwith *talent_scout*, see the [Searching with talent_scout](\n#searching-with-talent_scout) section below.\n\n[talent_scout]: https://rubygems.org/gems/talent_scout\n\n\n## Flash messages\n\nFlash messages are defined using I18n.  The *garden_variety* installer\n(`rails generate garden:install`) will create a\n\"config/locales/flash.en.yml\" file containing default \"success\" and\n\"error\" messages.  You can edit this file to customize those messages,\nor add your own translation files to support other languages.\n\nAs seen in the `PostsController#flash_message` method in the example\nabove, a prioritized list of keys are tried when retrieving a flash\nmessage.  Keys specific to the controller are tried first, followed by\nkeys specific to the action, and then finally generic status keys.  For\neach level of specificity, `*_html` key variants are supported, which\nallow raw HTML to be included in flash messages and not be escaped when\nrendered.  (See the [Safe HTML Translations] section of the Rails\nInternationalization guide for more information.)\n\nInterpolation in flash messages is also supported (as described by\n[Passing Variables to Translations]), with interpolation values provided\nby the `flash_options` method.  By default, `flash_options` provides a\n`model_name` value, but you can override it to provide your own values.\n\n[Safe HTML Translations]: https://guides.rubyonrails.org/i18n.html#using-safe-html-translations\n[Passing Variables to Translations]: https://guides.rubyonrails.org/i18n.html#passing-variables-to-translations\n\n\n## Beyond garden variety behavior\n\n*garden_variety* is designed to reduce the amount of custom code\nwritten, including in situations where custom code is unavoidable.\n\n\n### Eliminating N+1 queries\n\nWith proper Russian doll caching, [N+1 queries can be a feature](\nhttps://youtu.be/ktZLpjCanvg?t=4m27s).  But, if you need to eliminate\nan N+1 query by using eager loading, you can override the\n`find_collection` method:\n\n```ruby\nclass PostsController \u003c ApplicationController\n  garden_variety\n\n  def find_collection\n    super.includes(:author)\n  end\nend\n```\n\n\n### Pagination\n\nYou can integrate your your favorite pagination gem (*may I suggest\n[moar](https://rubygems.org/gems/moar)?*) by overriding the\n`find_collection` method:\n\n```ruby\nclass PostsController \u003c ApplicationController\n  garden_variety\n\n  def find_collection\n    moar(super.order(:created_at))\n  end\nend\n```\n\n\n### Searching\n\nYou can provide search functionality by overriding the `find_collection`\nmethod:\n\n```ruby\nclass PostsController \u003c ApplicationController\n  garden_variety\n\n  def find_collection\n    params[:title] ? super.where(\"title LIKE ?\", \"%#{params[:title]}%\") : super\n  end\nend\n```\n\n#### Searching with talent_scout\n\nIf you are using the [talent_scout] gem, the default implementation of\n`find_collection` will automatically instantiate your model search class\n-- no custom code required.  For example, if a `PostSearch` class is\ndefined, `PostsController#find_collection` will be equivalent to:\n\n```ruby\ndef find_collection\n  @search = PostSearch.new(params[:q])\n  @search.results\nend\n```\n\nNotice, as a side effect, the `@search` instance variable is set for\nlater use in the view.\n\nThe model search class will be chosen based on the controller's\n`::model_class`.  For example:\n\n```ruby\nclass MyPostsController \u003c ApplicationController\n  garden_variety\n\n  self.model_class = Post # find_collection will use PostSearch instead of MyPostSearch\nend\n```\n\nIf a corresponding model search class is not defined, `find_collection`\nwill fall back to its original non-search behavior.\n\nAlternatively, you can override the model search class directly:\n\n```ruby\nclass MyPostsController \u003c ApplicationController\n  garden_variety\n\n  self.model_class = Post\n  self.model_search_class = MyPostSearch # find_collection will use MyPostSearch\nend\n```\n\n\n### Server-generated JavaScript Responses (SJR)\n\nSJR controller actions are generally the same as conventional controller\nactions, with one difference: non-GET SJR actions render instead of\nredirect on success.  *garden_variety* provides a concise syntax for\noverriding only the on-success behavior of non-GET actions:\n\n```ruby\nclass PostsController \u003c ApplicationController\n  garden_variety\n\n  def create\n    super{ render plain: \"text\" } # renders text on success instead of redirect\n  end\nend\n```\n\nThus, combining this syntax with Rails' default rendering convention:\n\n```ruby\nclass PostsController \u003c ApplicationController\n  garden_variety\n\n  def create\n    respond_to do |format|\n      format.js{ super{} } # renders \"app/views/posts/create.js.erb\" on success\n    end\n  end\n\n  def update\n    respond_to do |format|\n      format.js{ super{} } # renders \"app/views/posts/update.js.erb\" on success\n    end\n  end\n\n  def destroy\n    respond_to do |format|\n      format.js{ super{} } # renders \"app/views/posts/destroy.js.erb\" on success\n    end\n  end\n\nend\n```\n\n**Sidenote:** *garden_variety* automatically manages the life of flash\nmessages.  If a redirect is replaced with a render, the flash message\nwill be set such that it does not affect the next request.\n\n\n### Authentication\n\nThe details of integrating authentication will depend on your chosen\nauthentication library.  [Devise](https://rubygems.org/gems/devise) is\nthe most popular, but [Clearance](https://rubygems.org/gems/clearance)\nis also a strong choice.  Whatever library you choose, it is likely to\ninclude a \"before filter\" which you must invoke in your controller to\nenforce authentication.  Something similar to the following:\n\n```ruby\nclass PostsController \u003c ApplicationController\n  garden_variety\n  before_action :authenticate_user!\nend\n```\n\nYour authentication library is also likely to provide a `current_user`\nmethod, which will return an appropriate value when the user is\nauthenticated.  Pundit automatically uses this method to enforce\nauthorization policies.  See your authentication library's documentation\nto verify that it provides this method, or see Pundit's documentation\nfor details on using a different method to identify the current user.\n\n*garden_variety* also provides a stub implementation of `current_user`,\nso that if no authentication library is chosen, `current_user` will be\ndefined to always return nil.\n\n**Note about Clearance:** Clearance versions previous to 2.0 define a\ndeprecated `authorize` method which conflicts with Pundit.  To avoid\nthis conflict, add the following line to your Clearance initializer:\n\n```ruby\n::Clearance::Authorization.send(:remove_method, :authorize)\n```\n\n\n### Form Objects\n\nThe Form Object pattern is used to mitigate the complexity of handling\nforms which need special processing logic, such as context-dependent\nvalidation, or forms which involve more than one model.  A detailed\nexplanation of the pattern is beyond the scope of this document, but\nconsider the following minimal example:\n\n```ruby\nclass RegistrationForm\n  include ActiveModel::Model\n\n  attr_accessor :email, :password, :accept_terms_of_service\n\n  validates :accept_terms_of_service, presence: true, acceptance: true\n\n  def save\n    @user = User.new(email: email, password: password)\n    if [valid?, @user.valid?].all?\n      @user.save\n    else\n      @user.errors.each{|attr, message| errors.add(attr, message) }\n      false\n    end\n  end\nend\n\n\nclass RegistrationFormsController \u003c ApplicationController\n  garden_variety :new, :create # only generate #new and #create actions\n\n  def create\n    super{ redirect_to root_path } # redirect to home page on success\n  end\nend\n```\n\nIn this example, only the `new` and `create` actions are generated by\nthe `garden_variety` macro.  The on-success behavior of `create` is\noverridden to redirect to `root_path`, using the concise syntax\ndiscussed in the [Integrating with SJR](#integrating-with-sjr) example.\nThe *garden_variety* controller helper methods all work as expected\nbecause `RegistrationForm` responds to `assign_attributes` and `save`,\nand has a default (nullary) constructor.\n\n\n### Non-REST actions\n\nYou may also define any non-REST controller actions you wish (i.e.\nactions other than: `index`, `show`, `new`, `create`, `edit`, `update`,\nand `destroy`).  The controller helper methods *garden_variety* provides\nmay be useful when doing so.  However, before implementing a non-REST\ncontroller action, consider if the behavior might be better implemented\nas a REST action in a new controller.  For example, instead of the\nfollowing `published` action...\n\n```ruby\nclass PostsController \u003c ApplicationController\n  garden_variety\n\n  def published\n    @posts = Post.where(published: true)\n    render :index\n  end\nend\n```\n\n...consider a new controller:\n\n```ruby\nclass PublishedPostsController \u003c ApplicationController\n  self.model_class = Post\n  garden_variety :index\n\n  def find_collection\n    super.where(published: true)\n  end\nend\n```\n\nNotice the call to `::model_class=`.  The model class for\n`PublishedPostsController` is overridden as `Post` instead of derived as\n`PublishedPost`.  And because of this override, the `@posts` instance\nvariable will be used instead of `@published_posts`.\n\nThis example may be somewhat contrived, but here is an excellent talk\nfrom RailsConf which delves deeper into the principle:\n[In Relentless Pursuit of REST](https://www.youtube.com/watch?v=HctYHe-YjnE)\n([slides](https://speakerdeck.com/derekprior/in-relentless-pursuit-of-rest)).\n\n\n## Installation\n\nAdd the gem to your Gemfile:\n\n```bash\n$ bundle add garden_variety\n```\n\nAnd run the install generator:\n\n```bash\n$ rails generate garden:install\n```\n\nThis will also run the Pundit install generator, if necessary.\n\n\n## Contributing\n\nRun `bin/test` to run the tests.\n\n\n## License\n\n[MIT License](MIT-LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjonathanhefner%2Fgarden_variety","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjonathanhefner%2Fgarden_variety","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjonathanhefner%2Fgarden_variety/lists"}