{"id":13482553,"url":"https://github.com/hashrocket/decent_exposure","last_synced_at":"2025-05-14T02:07:39.989Z","repository":{"id":777606,"uuid":"468319","full_name":"hashrocket/decent_exposure","owner":"hashrocket","description":"A helper for creating declarative interfaces in controllers","archived":false,"fork":false,"pushed_at":"2023-04-11T16:09:31.000Z","size":448,"stargazers_count":1807,"open_issues_count":9,"forks_count":108,"subscribers_count":38,"default_branch":"main","last_synced_at":"2025-04-23T21:39:55.290Z","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/hashrocket.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2010-01-12T02:47:04.000Z","updated_at":"2025-04-23T13:39:40.000Z","dependencies_parsed_at":"2023-07-05T20:02:53.966Z","dependency_job_id":null,"html_url":"https://github.com/hashrocket/decent_exposure","commit_stats":{"total_commits":376,"total_committers":64,"mean_commits":5.875,"dds":0.800531914893617,"last_synced_commit":"1d64ef2238d3eb6cf73c3887eb4398e94a2a1fd8"},"previous_names":["voxdolo/decent_exposure"],"tags_count":27,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hashrocket%2Fdecent_exposure","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hashrocket%2Fdecent_exposure/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hashrocket%2Fdecent_exposure/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hashrocket%2Fdecent_exposure/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hashrocket","download_url":"https://codeload.github.com/hashrocket/decent_exposure/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252288955,"owners_count":21724326,"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-07-31T17:01:03.185Z","updated_at":"2025-05-14T02:07:34.971Z","avatar_url":"https://github.com/hashrocket.png","language":"Ruby","readme":"![Decent Exposure](./doc/decent_exposure.png)\n\n[![Gem Version](https://img.shields.io/gem/v/decent_exposure.svg)](https://rubygems.org/gems/decent_exposure)\n[![Build Status](https://img.shields.io/github/workflow/status/hashrocket/decent_exposure/CI)](https://github.com/hashrocket/decent_exposure/actions?query=workflow%3ACI)\n\n### Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'decent_exposure', '~\u003e 3.0'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install decent_exposure\n\n### API\n\nThe whole API consists of three methods so far: `expose`, `expose!`, and\n`exposure_config`.\n\nIn the simplest scenario you'll just use it to expose a model in the\ncontroller:\n\n```ruby\nclass ThingsController \u003c ApplicationController\n  expose :thing\nend\n```\n\nNow every time you call `thing` in your controller or view, it will look for an\nID and try to perform `Thing.find(id)`. If the ID isn't found, it will call\n`Thing.new(thing_params)`. The result will be memoized in an `@exposed_thing`\ninstance variable.\n\n#### Example Controller\n\nHere's what a standard Rails 5 CRUD controller using Decent Exposure might look like:\n\n```ruby\nclass ThingsController \u003c ApplicationController\n  expose :things, -\u003e{ Thing.all }\n  expose :thing\n\n  def create\n    if thing.save\n      redirect_to thing_path(thing)\n    else\n      render :new\n    end\n  end\n\n  def update\n    if thing.update(thing_params)\n      redirect_to thing_path(thing)\n    else\n      render :edit\n    end\n  end\n\n  def destroy\n    thing.destroy\n    redirect_to things_path\n  end\n\n  private\n\n  def thing_params\n    params.require(:thing).permit(:foo, :bar)\n  end\nend\n```\n\n### Under the Hood\n\nThe default resolving workflow is pretty powerful and customizable. It could be\nexpressed with the following pseudocode:\n\n```ruby\ndef fetch(scope, id)\n  instance = id ? find(id, scope) : build(build_params, scope)\n  decorate(instance)\nend\n\ndef id\n  params[:thing_id] || params[:id]\nend\n\ndef find(id, scope)\n  scope.find(id)\nend\n\ndef build(params, scope)\n  scope.new(params) # Thing.new(params)\nend\n\ndef scope\n  model # Thing\nend\n\ndef model\n  exposure_name.classify.constantize # :thing -\u003e Thing\nend\n\ndef build_params\n  if respond_to?(:thing_params, true) \u0026\u0026 !request.get?\n    thing_params\n  else\n    {}\n  end\nend\n\ndef decorate(thing)\n  thing\nend\n```\n\nThe exposure is also lazy, which means that it won't do anything until you call\nthe method. To eliminate this laziness you can use the `expose!` macro instead,\nwhich will try to resolve the exposure in a before filter.\n\nIt is possible to override each step with options. The acceptable options to the\n`expose` macro are:\n\n### `fetch`\n\nThis is the entry point. The `fetch` proc defines how to resolve your exposure\nin the first place.\n\n```ruby\nexpose :thing, fetch: -\u003e{ get_thing_some_way_or_another }\n```\n\nBecause the above behavior overrides the normal workflow, all other options\nwould be ignored. However, Decent Exposure is decent enough to actually blow\nup with an error so you don't accidentally do this.\n\nThere are other less verbose ways to pass the `fetch` block, since you'll\nprobably be using it often:\n\n```ruby\nexpose(:thing){ get_thing_some_way_or_another }\n```\n\nOr\n\n```ruby\nexpose :thing, -\u003e{ get_thing_some_way_or_another }\n```\n\nOr even shorter\n\n```ruby\nexpose :thing, :get_thing_some_way_or_another\n```\n\nThere is another shortcut that allows you to redefine the entire fetch block\nwith less code:\n\n```ruby\nexpose :comments, from: :post\n# equivalent to \nexpose :comments, -\u003e{ post.comments }\n```\n\n### `id`\n\nThe default fetch logic relies on the presence of an ID. And of course Decent\nExposure allows you to specify how exactly you want the ID to be extracted.\n\nDefault behavior could be expressed using following code:\n\n```ruby\nexpose :thing, id: -\u003e{ params[:thing_id] || params[:id] }\n```\n\nBut nothing is stopping you from throwing in any arbitrary code:\n\n```ruby\nexpose :thing, id: -\u003e{ 42 }\n```\n\nPassing lambdas might not always be fun, so here are a couple of shortcuts that\ncould help make life easier.\n\n```ruby\nexpose :thing, id: :custom_thing_id\n# equivalent to\nexpose :thing, id: -\u003e{ params[:custom_thing_id] }\n\nexpose :thing, id: [:try_this_id, :or_maybe_that_id]\n# equivalent to\nexpose :thing, id: -\u003e{ params[:try_this_id] || params[:or_maybe_that_id] }\n```\n\n### `find`\n\nIf an ID was provided, Decent Exposure will try to find the model using it.\nDefault behavior could be expressed with this configuration: \n\n```ruby\nexpose :thing, find: -\u003e(id, scope){ scope.find(id) }\n```\n\nWhere `scope` is a model scope, like `Thing` or `User.active` or\n`Post.published`.\n\nNow, if you're using FriendlyId or Stringex or something similar, you'd have to\ncustomize your finding logic. Your code might look somewhat like this:\n\n```ruby\nexpose :thing, find: -\u003e(id, scope){ scope.find_by!(slug: id) }\n```\n\nAgain, because this is likely to happen a lot, Decent Exposure gives you a\ndecent shortcut so you can get more done by typing less.\n\n```ruby\nexpose :thing, find_by: :slug\n```\n\n### `build`\n\nWhen an ID is not present, Decent Exposure tries to build an object for you.\nBy default, it behaves like this:\n\n```ruby\nexpose :thing, build: -\u003e(thing_params, scope){ scope.new(thing_params) }\n```\n\n### `build_params`\n\nThese options are responsible for calulating params before passing them to the\nbuild step. The default behavior was modeled with Strong Parameters in mind and\nis somewhat smart: it calls the `thing_params` controller method if it's\navailable and the request method is not `GET`. In all other cases it produces\nan empty hash.\n\nYou can easily specify which controller method you want it to call instead of\n`thing_params`, or just provide your own logic:\n\n```ruby\nexpose :thing, build_params: :custom_thing_params\nexpose :other_thing, build_params: -\u003e{ { foo: \"bar\" } }\n\nprivate\n\ndef custom_thing_params\n  # strong parameters stuff goes here\nend\n```\n\n### `scope`\n\nDefines the scope that's used in `find` and `build` steps.\n\n```ruby\nexpose :thing, scope: -\u003e{ current_user.things }\nexpose :user, scope: -\u003e{ User.active }\nexpose :post, scope: -\u003e{ Post.published }\n```\n\nLike before, shortcuts are there to make you happier:\n\n```ruby\nexpose :post, scope: :published\n# equivalent to\nexpose :post, scope: -\u003e{ Post.published }\n```\n\nand\n\n```ruby\nexpose :thing, parent: :current_user\n# equivalent to:\nexpose :thing, scope: -\u003e{ current_user.things }\n```\n\n### `model`\n\nAllows you to specify the model class to use. Pretty straightforward.\n\n```ruby\nexpose :thing, model: -\u003e{ AnotherThing }\nexpose :thing, model: AnotherThing\nexpose :thing, model: \"AnotherThing\"\nexpose :thing, model: :another_thing\n```\n\n### `decorate`\n\nBefore returning the thing, Decent Exposure will run it through the\ndecoration process. Initially, this does nothing, but you can obviously change\nthat:\n\n```ruby\nexpose :things, -\u003e{ Thing.all.map{ |thing| ThingDecorator.new(thing) } }\nexpose :thing, decorate: -\u003e(thing){ ThingDecorator.new(thing) }\n```\n\n## `exposure_config`\n\nYou can pre-save some configuration with `exposure_config` method to reuse it\nlater.\n\n```ruby\nexposure_config :cool_find, find: -\u003e{ very_cool_find_code }\nexposure_config :cool_build, build: -\u003e{ very_cool_build_code }\n\nexpose :thing, with: [:cool_find, :cool_build]\nexpose :another_thing, with: :cool_build\n```\n\n## Rails Mailers\n\nMailers and Controllers use the same decent_exposure dsl.\n\n### Example Mailer\n\n```ruby\nclass PostMailer \u003c ApplicationMailer\n  expose(:posts, -\u003e { Post.last(10) })\n  expose(:post)\n\n  def top_posts\n    @greeting = \"Top Posts\"\n    mail to: \"to@example.org\"\n  end\n\n  def featured_post(id:)\n    @greeting = \"Featured Post\"\n    mail to: \"to@example.org\"\n  end\nend\n```\n\n## Rails Scaffold Templates\n\nIf you want to generate rails scaffold templates prepared for `decent_exposure` run:\n\n```bash\nrails generate decent_exposure:scaffold_templates [--template_engine erb|haml]\n```\n\nThis will create the templates in your `lib/templates` folder.\n\nMake sure you have configured your templates engine for generators in `config/application.rb`:\n\n```ruby\n# config/application.rb\nconfig.generators do |g|\n  g.template_engine :erb\nend\n```\n\nNow you can run scaffold like:\n\n```bash\nrails generate scaffold post title description:text\n```\n\n## Contributing\n\n1. Fork it (https://github.com/hashrocket/decent_exposure/fork)\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create a new Pull Request\n\n## About\n\n[![Hashrocket logo](https://hashrocket.com/hashrocket_logo.svg)](https://hashrocket.com)\n\nDecent Exposure is supported by the team at [Hashrocket](https://hashrocket.com), a multidisciplinary design \u0026 development consultancy. If you'd like to [work with us](https://hashrocket.com/contact) or [join our team](https://hashrocket.com/careers), don't hesitate to get in touch.\n","funding_links":[],"categories":["Decorators","Ruby","Abstraction"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhashrocket%2Fdecent_exposure","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhashrocket%2Fdecent_exposure","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhashrocket%2Fdecent_exposure/lists"}