• Stars
    star
    1,799
  • Rank 24,805 (Top 0.6 %)
  • Language
    Ruby
  • License
    MIT License
  • Created over 14 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A helper for creating declarative interfaces in controllers

Decent Exposure

Gem Version Build Status

Installation

Add this line to your application's Gemfile:

gem 'decent_exposure', '~> 3.0'

And then execute:

$ bundle

Or install it yourself as:

$ gem install decent_exposure

API

The whole API consists of three methods so far: expose, expose!, and exposure_config.

In the simplest scenario you'll just use it to expose a model in the controller:

class ThingsController < ApplicationController
  expose :thing
end

Now every time you call thing in your controller or view, it will look for an ID and try to perform Thing.find(id). If the ID isn't found, it will call Thing.new(thing_params). The result will be memoized in an @exposed_thing instance variable.

Example Controller

Here's what a standard Rails 5 CRUD controller using Decent Exposure might look like:

class ThingsController < ApplicationController
  expose :things, ->{ Thing.all }
  expose :thing

  def create
    if thing.save
      redirect_to thing_path(thing)
    else
      render :new
    end
  end

  def update
    if thing.update(thing_params)
      redirect_to thing_path(thing)
    else
      render :edit
    end
  end

  def destroy
    thing.destroy
    redirect_to things_path
  end

  private

  def thing_params
    params.require(:thing).permit(:foo, :bar)
  end
end

Under the Hood

The default resolving workflow is pretty powerful and customizable. It could be expressed with the following pseudocode:

def fetch(scope, id)
  instance = id ? find(id, scope) : build(build_params, scope)
  decorate(instance)
end

def id
  params[:thing_id] || params[:id]
end

def find(id, scope)
  scope.find(id)
end

def build(params, scope)
  scope.new(params) # Thing.new(params)
end

def scope
  model # Thing
end

def model
  exposure_name.classify.constantize # :thing -> Thing
end

def build_params
  if respond_to?(:thing_params, true) && !request.get?
    thing_params
  else
    {}
  end
end

def decorate(thing)
  thing
end

The exposure is also lazy, which means that it won't do anything until you call the method. To eliminate this laziness you can use the expose! macro instead, which will try to resolve the exposure in a before filter.

It is possible to override each step with options. The acceptable options to the expose macro are:

fetch

This is the entry point. The fetch proc defines how to resolve your exposure in the first place.

expose :thing, fetch: ->{ get_thing_some_way_or_another }

Because the above behavior overrides the normal workflow, all other options would be ignored. However, Decent Exposure is decent enough to actually blow up with an error so you don't accidentally do this.

There are other less verbose ways to pass the fetch block, since you'll probably be using it often:

expose(:thing){ get_thing_some_way_or_another }

Or

expose :thing, ->{ get_thing_some_way_or_another }

Or even shorter

expose :thing, :get_thing_some_way_or_another

There is another shortcut that allows you to redefine the entire fetch block with less code:

expose :comments, from: :post
# equivalent to 
expose :comments, ->{ post.comments }

id

The default fetch logic relies on the presence of an ID. And of course Decent Exposure allows you to specify how exactly you want the ID to be extracted.

Default behavior could be expressed using following code:

expose :thing, id: ->{ params[:thing_id] || params[:id] }

But nothing is stopping you from throwing in any arbitrary code:

expose :thing, id: ->{ 42 }

Passing lambdas might not always be fun, so here are a couple of shortcuts that could help make life easier.

expose :thing, id: :custom_thing_id
# equivalent to
expose :thing, id: ->{ params[:custom_thing_id] }

expose :thing, id: [:try_this_id, :or_maybe_that_id]
# equivalent to
expose :thing, id: ->{ params[:try_this_id] || params[:or_maybe_that_id] }

find

If an ID was provided, Decent Exposure will try to find the model using it. Default behavior could be expressed with this configuration:

expose :thing, find: ->(id, scope){ scope.find(id) }

Where scope is a model scope, like Thing or User.active or Post.published.

Now, if you're using FriendlyId or Stringex or something similar, you'd have to customize your finding logic. Your code might look somewhat like this:

expose :thing, find: ->(id, scope){ scope.find_by!(slug: id) }

Again, because this is likely to happen a lot, Decent Exposure gives you a decent shortcut so you can get more done by typing less.

expose :thing, find_by: :slug

build

When an ID is not present, Decent Exposure tries to build an object for you. By default, it behaves like this:

expose :thing, build: ->(thing_params, scope){ scope.new(thing_params) }

build_params

These options are responsible for calulating params before passing them to the build step. The default behavior was modeled with Strong Parameters in mind and is somewhat smart: it calls the thing_params controller method if it's available and the request method is not GET. In all other cases it produces an empty hash.

You can easily specify which controller method you want it to call instead of thing_params, or just provide your own logic:

expose :thing, build_params: :custom_thing_params
expose :other_thing, build_params: ->{ { foo: "bar" } }

private

def custom_thing_params
  # strong parameters stuff goes here
end

scope

Defines the scope that's used in find and build steps.

expose :thing, scope: ->{ current_user.things }
expose :user, scope: ->{ User.active }
expose :post, scope: ->{ Post.published }

Like before, shortcuts are there to make you happier:

expose :post, scope: :published
# equivalent to
expose :post, scope: ->{ Post.published }

and

expose :thing, parent: :current_user
# equivalent to:
expose :thing, scope: ->{ current_user.things }

model

Allows you to specify the model class to use. Pretty straightforward.

expose :thing, model: ->{ AnotherThing }
expose :thing, model: AnotherThing
expose :thing, model: "AnotherThing"
expose :thing, model: :another_thing

decorate

Before returning the thing, Decent Exposure will run it through the decoration process. Initially, this does nothing, but you can obviously change that:

expose :things, ->{ Thing.all.map{ |thing| ThingDecorator.new(thing) } }
expose :thing, decorate: ->(thing){ ThingDecorator.new(thing) }

exposure_config

You can pre-save some configuration with exposure_config method to reuse it later.

exposure_config :cool_find, find: ->{ very_cool_find_code }
exposure_config :cool_build, build: ->{ very_cool_build_code }

expose :thing, with: [:cool_find, :cool_build]
expose :another_thing, with: :cool_build

Rails Mailers

Mailers and Controllers use the same decent_exposure dsl.

Example Mailer

class PostMailer < ApplicationMailer
  expose(:posts, -> { Post.last(10) })
  expose(:post)

  def top_posts
    @greeting = "Top Posts"
    mail to: "[email protected]"
  end

  def featured_post(id:)
    @greeting = "Featured Post"
    mail to: "[email protected]"
  end
end

Rails Scaffold Templates

If you want to generate rails scaffold templates prepared for decent_exposure run:

rails generate decent_exposure:scaffold_templates [--template_engine erb|haml]

This will create the templates in your lib/templates folder.

Make sure you have configured your templates engine for generators in config/application.rb:

# config/application.rb
config.generators do |g|
  g.template_engine :erb
end

Now you can run scaffold like:

rails generate scaffold post title description:text

Contributing

  1. Fork it (https://github.com/hashrocket/decent_exposure/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

About

Hashrocket logo

Decent Exposure is supported by the team at Hashrocket, a multidisciplinary design & development consultancy. If you'd like to work with us or join our team, don't hesitate to get in touch.

More Repositories

1

gatling

Deployment tool for Phoenix apps
Elixir
500
star
2

tilex

Today I Learned
Elixir
490
star
3

websocket-shootout

A comparison of websocket servers in multiple languages and frameworks
JavaScript
417
star
4

ws

websocket command line tool
Go
416
star
5

dotmatrix

Hashrocket Dot Files
Vim Script
363
star
6

hr-til

Today I Learned in Ruby
Ruby
342
star
7

slurper

Gem for slurping plain text stories into Pivotal Tracker
Ruby
127
star
8

view_specify

Auto-generate RSpec view specs by interrogating your existing views.
Ruby
55
star
9

unencumbered

Just enough Cucumber in RSpec.
Ruby
53
star
10

capybara-webmock

Mock external requests for Capybara JavaScript drivers
Ruby
48
star
11

mousetrap

CheddarGetter API Client in Ruby
Ruby
34
star
12

namecheap

ruby namecheap API wrapper
Ruby
32
star
13

coming-soon

App to register emails addresses. Intended as a coming soon, pre-launch, splash page.
JavaScript
27
star
14

acts_as_featured

Ruby
25
star
15

design_patterns_in_ruby

Wherein we reimplement the design patterns from the _Design Patterns in Ruby_ book, by Russ Olsen.
Ruby
25
star
16

vim-hashrocket

Mappings we find useful
Vim Script
23
star
17

localpolitics.in

Hashrocket's Martin Luther King Jr. Memorial 2009 Hack Day Apps for America Celebrity Rabies Awareness Pro-Am Fun Run Race For The Cure
JavaScript
18
star
18

terraformation

Generators with a Hashrocket twist
Ruby
15
star
19

vim-macdown

write markdown in Vim with live-reloads in MacDown
Vim Script
14
star
20

hashrocket-rails

Rails engine & generators for bootstrapping a Hashrocket project
Ruby
12
star
21

slack-command-api

A Sinatra API for processing custom Slack commands
Ruby
12
star
22

boot_devcards_example

Example of using devcards with boot
Clojure
10
star
23

graphql_way_rails

This is a Proof of Concept Rails project using GraphQL
Ruby
6
star
24

hr_hotels

Example database for hotels.
Ruby
5
star
25

wowza

Ruby wrapper around the Wowza REST API
Ruby
5
star
26

hr

Hashrocket Sub
Ruby
5
star
27

ecto_pg_extras

A collection of custom functions for PostgreSQL features in Ecto
Elixir
4
star
28

my_emma

Ruby wrapper for the MyEmma Remote Signup API
Ruby
4
star
29

university-bookstore

Hashrocket Training Project for Pre-RailsConf 2010 Workshop
Ruby
4
star
30

hashrocket-vr-example

JavaScript
3
star
31

dibs-ios

An iOS client for the Dibs "Online classifieds without the strangers" web application.
Objective-C
3
star
32

OffBot

🔌 🤖 Timely slack notifications for scheduled holidays and vacations
Ruby
3
star
33

learn_to_program

Curriculums and Programs for Teaching people how to build web applications
Ruby
2
star
34

vim-hr-psql

Open psql formatted table definitions from Vim Postgres
Vim Script
2
star
35

wowza-webhooks

Java
2
star
36

concepts

A gallery for our side projects
JavaScript
2
star
37

hacktive

Github activity tracker
Ruby
1
star
38

chime

Chimes in the Chicago office when someone comes in the front door
Arduino
1
star
39

homebrew-formulas

Hashrocket homebrew formulas
Ruby
1
star
40

vostok

The Hashrocket middleman template
CSS
1
star
41

spices

Ruby
1
star
42

metro_relic

Easily track custom newrelic metrics with a config file
Ruby
1
star
43

trybool

The value parsing tool to return a boolean that you never knew you needed!
Ruby
1
star
44

homebrew-fdw

FDW formulas for Homebrew package manager
Ruby
1
star
45

scored

Simple iPhone app
Objective-C
1
star
46

hashshake

Hashrocket shakes hands 🤝
Ruby
1
star