Skip to content
Jeremie edited this page Mar 13, 2014 · 75 revisions

Gin is a small Ruby web framework, built on Rack, which borrows from Sinatra expressiveness, and targets larger applications.

Getting Started

Gin provides a simple command for setting up a new application.

  $ gin path/to/appname

Hello World

A Hello World application requires at minimum a Gin::App instance and a Gin::Controller.

  # my_app.rb
  class HelloWorld < Gin::Controller
    def index; "Hello World"; end
  end

  class MyApp < Gin::App
    mount HelloWorld, "/"
  end

  # config.ru
  require './my_app'
  run MyApp.new

Why Gin?

Gin tries to fill a need for a framework that's simple and lightweight, yet scalable for large applications. Gin is comparable to Sinatra in performance and memory consumption, but supports multiple controllers and filters.

Features

  • Easily mount multiple apps as Rack middleware
  • Simple and fast RESTful routing
  • Controller classes and action arguments
  • Inheritable Before/After filter chains
  • Response streaming
  • Asset urls with cache-busting and CDN support
  • Asset pipelining
  • Configs for multiple environments
  • Periodic config reloading
  • Inheritable error handling
  • In-app middleware
  • Rack-Session and Rack-Protection
  • Dev-mode auto-reloading
  • Templates (layouts/views) with Tilt

What's Not Included

  • Helpers - those are called Modules in Ruby
  • ORM - choose your own library

Some Rough Specs

All specs are based on a simple Hello World application running with Puma-2.3.2 (1 worker, max 16 threads) on Ruby 1.9.3, on a 2.8GHz Core i7, OSX 10.7.5.

Object Allocations

  • Rack lambda app (baseline): 11 alloc/req
  • Out of the box (prod): 153 alloc/req
  • Out of the box (dev w/ reloading): 741 alloc/req
  • With Rack::Session / Rack::Protection (prod): 357 alloc/req
  • Sinatra 1.4.3 (prod): 198 alloc/req
  • Rails 3.2.13 (prod): 1078 alloc/req
  • Rails 4.0.0 (prod): 1179 alloc/req

Memory Usage

Memory measured after warming up the app with 15000 requests.

  • Rack lambda app (baseline): 42Mb
  • Out of the box (prod): 48Mb
  • Out of the box (dev w/ reloading): 68Mb
  • With Rack::Session / Rack::Protection (prod): 53Mb
  • Sinatra 1.4.3 (prod): 51Mb
  • Rails 3.2.13 (prod): 110Mb
  • Rails 4.0.0 (prod): 82Mb
  • Rubinius 2.0.0-rc1 (prod): 70Mb

Throughput

Benchmarking was run using 16 concurrent connections.

  • Rack lambda app (baseline): 3763 req/s
  • Out of the box (prod): 2279 req/s
  • Out of the box (dev w/ reloading): 547 req/s
  • With Rack::Session / Rack::Protection (prod): 1572 req/s
  • Sinatra 1.4.3 (prod): 2111 req/s
  • Rails 3.2.13 (prod): 622 req/s
  • Rails 4.0.0 (prod): 526 req/s
  • Rubinius 2.0.0-rc1 (prod): 1532 req/s

App Setup

The App class is the central location for setting up loading and configuration of your web app. Your app must inherit Gin::App, and load and mount the controllers from that class.

Routes

Routes are defined in the application class with the use of the 'mount' method. This method takes for argument a controller class, an optional base path, and an optional block. By default, Gin assigns a route to all actions in a controller following the REST pattern, and uses the controller name for the base path. A block may be given to specify custom routes

  class FooApp < Gin::App
    require 'foo_app/controllers/home'
    require 'foo_app/controllers/user'

    # Creates only defined routes, then adds all defaults for remaining actions.
    mount HomeController, "/" do
      any :home,    "/"             # Routes all verbs with root path to home
      get :login,   "/session/new"
      get :article, "/news/:id"
      defaults
    end

    # Creates RESTful routes under /user, such as GET /user/:id
    mount UserController
  end

Gin assigns names to these routes by default (e.g. UserController#show will be named :show_user), but custom names are also supported by passing a second Symbol argument to the routing methods.

  class FooApp < Gin::App
    require 'foo_app/controllers/home_controller'

    mount HomeController, "/" do
      get :index, :home
      post :login, "/session/new", :create_session
    end
  end

Environment

The App environment name is accessible both from the App instance and class. It defaults to ENV['RACK_ENV'] or 'development' if no RACK_ENV is defined.

  environment
  environment 'test'

Helper instance methods for checking the environment.

  @app.development?
  @app.test?
  @app.staging?
  @app.production?

App Middleware

Gin Apps support their own internal Rack middleware stack, which is empty by default. This stack does not run if no route is found and your app is attached to the main Rack stack as middleware.

You can mount your own middleware to the App's stack with the 'use' class method, similar to Rack's implementation.

  use MyMiddleware, *args

Gin also comes with Rack::Cookies::Session and Rack::Protection middlewares built-in but turned off by default. You can enable these with the following class methods.

Set using Rack sessions or not. Defaults to false.

  sessions true
  sessions :domain => 'foo.com'

Get or set the session secret. Defaults to a new random String on boot.

  session_secret
  session_secret "SOME LONG SECRET STRING"

Set using Rack::Protection or not. Defaults to false.

  protection true
  protection :except => [:path_traversal, :session_hijacking]

Error Delegate

The error delegate is a controller used for handling non-existing routes and otherwise uncaught errors. It defaults to Gin::Controller but it may be useful to set to your ApplicationController if you have one.

  error_delegate
  error_delegate AppController

More App Attributes

Get or set the current root dir. Defaults to the app script's directory.

  root_dir
  root_dir "/path/to/new/dir"

Get or set the current public dir. Defaults to root_dir + '/public'

  public_dir
  public_dir "/path/to/public"

Get or set the current config dir. Defaults to root_dir + '/config'

  config_dir
  config_dir "/path/to/configs"

Get or set the current layouts dir. Defaults to root_dir + '/layouts'. Wildcards "*" in the path are replaced by the controller name at render time to allow per-controller directories.

  layouts_dir
  layouts_dir "/path/to/layouts"

Get or set the current views dir. Defaults to root_dir + '/views'. Wildcards "*" in the path are replaced by the controller name at render time to allow per-controller directories.

  views_dir
  views_dir "/path/to/views"

Get or set the asset host or asset host rules.

  asset_host
  asset_host "http://assets.example.com"
  asset_host do |path|
    # do something based on the asset path/name
  end

Add a custom mime-type definition.

  mime_type :json, "application/json"

Get or set the Host header on responses if not set by the called action. If the enforce options is given, the app will only respond to requests that explicitly match the specified host, or regexp. This is useful for running multiple apps on the same middleware stack that might have conflicting routes, or need explicit per-host routing (such as an admin app).

  hostname 'host.com'
  hostname 'admin.host.com:443', :enforce => true
  hostname 'admin.host.com', :enforce => /^admin\.(localhost|host\.com)/

Controllers and Actions

Gin controllers follow only a few rules:

  • ALL instance methods are actions (put your helper methods in a module or parent class not mounted to the app).
  • Filters are defined by blocks passed to special class methods.
  • Controller-level error handlers are defined by blocks passed to the 'error' class method.

Gin controller actions are any instance method defined on the controller class. The HTTP response body takes the value of the method's return value.

If the instance method takes arguments, matching params will be assigned to them. A required argument with a missing param triggers a Gin::BadRequest error, resulting in a 400 response.

  class UserController < Gin::Controller
    # Get params id and email
    def show id, email=nil
      ...
    end
  end

Gin actions also support Ruby 2.0 keyed arguments, which are more flexible for assigning default values when dealing with missing params.

  class UserController < Gin::Controller
    def show(id, email: nil, full: false)
      ...
    end
  end

Views and Layouts

Views are rendered by calling the 'view' method from a controller instance. Views don't halt the action but return a String of the rendered view. If a layout was set, the rendered view will include the layout.

  class UserController < Gin::Controller
    def show id
      # Renders <app.views_dir>/show_user.* with the app's default layout, if it exists.
      view :show_user
    end
  end

Layouts are defined in the App, under its 'default_layout' class method, but can also be set at the controller level:

  class UserController < Gin::Controller
    layout :user

    def show id
      # Renders <app.views_dir>/show_user.* with the layout <app.layouts_dir>/user.*
      view :show_user
    end
  end

Layouts can also be overridden from the view method:

  class UserController < Gin::Controller
    layout :user

    def show id
      # Renders <app.views_dir>/show_user.* with the layout <app.layouts_dir>/custom.*
      view :show_user, :layout => :custom
    end

    def tos
      # Renders <app.views_dir>/tos.* with no layout
      view :tos, :layout => false
    end
  end

By default, the view is rendered with the controller instance as scope. A scope and locals may also be passed to the view method:

  class UserController < Gin::Controller
    def show(id, email: nil, full: false)
      @user = User.find(id)
      view :user, scope: @user, locals: {time: Time.now}
    end
  end

Paths and URLs

Generating paths that reference other controllers and actions is handled with the 'path_to' helper method.

  path_to UserController, :index
  path_to UserController, :show, :id => 123  # Required path params will throw an error if omitted
  path_to :show_user, :id => 123             # Named route

  # Controller name may be omitted if generating path to the same controller
  path_to :show, :id => 123

  # You can also use a preset path and append params
  path_to '/articles', :page => 2
  #=> '/articles?page=2'

Full URLs are generated in the same manner with the 'url_to' method, also aliased as 'to'.

  url_to UserController, :show, :id => 123
  #=> http://example.com/user/123

Rewrite

Rewriting a request from the controller effectively halts execution of the current controller action and creates a new request to another action and/or controller, or path.

The rewrite method acts just as if a request had been sent from the client, and will re-run any of the in-app middleware. If the app is itself running as middleware, you may use rewrite to pass a request down to the next item in the stack by specifying a path not supported by the app.

  rewrite MyController, :action
  #=> Calls app with route for MyController#action

  rewrite MyController, :show, :id => 123
  #=> Calls app with route for MyController#action with the given params

  rewrite :show_foo
  #=> Calls app with route for the current controller's :show_foo action,
  #=> or if missing the controller and action for the :show_foo named route.

  # Rewrite and execute the request with the given headers.
  rewrite :show_foo, params, 'HTTP_X_CUSTOM_HEADER' => 'foo'

  # Rewrite to an arbitrary path.
  rewrite '/path/to/something/else', {}, 'REQUEST_METHOD' => 'POST'

Note that params are not forwarded with the rewrite call. The app considers this to be a completely different request, which means all params required must be passed explicitly.

Streamed and IO request content is also ignored unless it is explicitly assigned to the 'rack.input' (as a part of the headers argument).

Reroute

Unlike Gin::Controller#rewrite, the reroute method forwards the current request and params to the provided controller and/or action, or named route, and halts further execution in the current action.

  reroute MyController, :action
  #=> Executes MyController#action

  reroute MyController, :show, :id => 123
  #=> Executes MyController#action with the given params merged to
  #=> the current params.

  reroute :show_foo
  #=> Executes the current controller's :show_foo action, or if missing
  #=> the controller and action for the :show_foo named route.

  # Reroute with the given headers.
  reroute :show_foo, {}, 'HTTP_X_CUSTOM_HEADER' => 'foo'

Asset Paths

Asset paths and urls are built by the 'asset_url' helper method. If the file is in the public directory defined by the app class, appends a unique id to the path from the file's md5. If the file is in the assets directory, returns the path to that file. Returns nil if the file is not present.

The app's public directory defaults to root_dir + "/public". The app's assets directory defaults to public_dir + "/assets".

  class MyApp < Gin::App
    public_dir "/path/to/my/publicdir/"
  end

  class NewsController < Gin::Controller
    def show id
      asset_url "news-#{id}.jpg"
      #=> "/news-123.jpg?lk241"
    end
  end

The app can also define a CDN or CDN rules which will be applied through the 'asset_url'.

  class MyApp < Gin::App
    asset_host "http://assets.example.com/"
  end

  class NewsController < Gin::Controller
    def show id
      asset_url "news-#{id}.jpg"
      #=> "http://assets.example.com/news-123.jpg"
    end
  end

Asset host rules are defined by giving the app's 'asset_host' definition a block.

  class MyApp < Gin::App
    asset_host do |path|
      case path
      when /\.(jpe?g|gif|png|bmp|tiff)$/
        "http://img.example.com"
      when /\.js$/
        "http://example.com/js"
      end
    end
  end

  class NewsController < Gin::Controller
    def show id
      asset_url "news-#{id}.jpg"
      #=> "http://img.example.com/news-123.jpg"
    end
  end

Asset Pipeline

As of release 1.2.0, Gin supports asset pipelining with the Sprockets gem. See the Asset Paths section above for generating paths to assets.

The asset pipeline requires no work to setup. At boot time, Gin will look for possible asset source directories (defined by App.asset_paths). If an asset source is found, the assets will be rendered automatically, otherwise the asset pipeline logic will not even be loaded. The asset pipeline behavior is easily controlled through the asset_pipeline App class method.

class MyApp < Gin::App
  asset_pipeline true    # Force ON
  asset_pipeline false   # Force OFF
end

Even if the asset_pipeline property is set to false, the Gin app will serve files in the directory assigned to App.assets_dir, which gives you the flexibility to render assets as you see fit. In other words: regardless of how assets were generated, Gin will generate paths to and serve any file under the App's assets_dir.

No doubt you will need to fine tune your pipeline. To this end, Gin yields a Sprockets::Environment instance to the asset_pipeline method.

asset_pipeline do |sprockets|
  sprockets.append_path 'foo/assets'
end

The assets_dir where Gin looks for rendered assets defaults to public_dir + "/assets" but is easily customized:

MyApp.assets_dir "/path/to/custom/dir"

Finally your app needs to know where to get the asset sources from. For this purpose, any number of directory globs may be given to the asset_paths method.

MyApp.asset_paths "/path/to/js_files/*/", "/path/to/css_files/*/"

Working with Requests

Although the 'request' is available through an accessor method on the controller, params, sessions, and cookies also have convenient helper and accessor methods.

Params

Params are accessible via the controller's 'params' instance method.

  params # Returns indifferent access param Hash

Gin controllers will by default try to identify and cast params to their appropriate type based on their value. The supported types are String, Integer, Float, TrueClass, and FalseClass. However, there are times when exceptions are required and one or more params must remain in their original String type. The Gin::Controller class provides an inheritable attribute 'autocast_params' which allows turning this feature on and off for all or a select number of params.

  autocast_params true   # enable for all params
  autocast_params false  # disable for all params

  # skip param type-casting for select params
  autocast_params :except => [:zip, :phone, :fax]

  # only type-cast select params
  autocast_params :only => [:age, :timestamp]

Sessions & Cookies

These are accessed via the 'session' and 'cookies' helper methods, which return and set the Rack request values. The 'params' accessor includes path params as defined in the routes.

  # Session is only available if enabled from the app.
  session # Returns the session hash
  session[:user] = @user.email # Sets and hashes the user session value

  cookies # Returns the cookies hash
  set_cookie "mycookie", "value", :expires => Time.now+300
  delete_cookie "mycookie"

The session secret is generated automatically by the app class on boot, or can be set from the app class.

  class MyApp < Gin::App
    session_secret "THIS IS MY LONG SECRET STRING"
  end

Setting the Response

Status, Headers, Body

There are multiple ways to set the response from a controller instance, including accessing the 'response' attribute. The easiest way is to let the action return a String or Rack response Array, but status, headers, and body can be set individually with the helper methods of the same name.

  def show id
    @news = News.first(id)
    if !@news
      status 404
      headers 'Content-Length' => '9'
      body "Not found"
      return
    end
    ...
  end

Correct uses of response methods:

  body # Returns currently set body
  body "Some string here"

  status # Returns currently set status
  status 404

  headers # Returns currently set headers
  headers 'Header-Name' => 'value'

Using these methods without arguments act as an accessor to see the currently set value.

Streaming

Streaming is handled by a 'stream' block which yields an IO-like object to write to. The 'stream' method sets the body with a Gin::Stream instance which only starts reading when the method returns.

  def download_many
    stream do |io|
      file = File.open "somefile", "rb"
      io << file.read(1024) until file.eof?
      file.close
    end
  end

A streaming connection can be kept open by passing true-ish as an argument to the 'stream' method. Useful for event-driven streaming.

Other Response Helpers

There are a few more response helpers, mostly involving header setting.

  content_type # Returns the currently set Content-Type header
  content_type :html
  content_type 'application/json', :default => :html

  last_modified Time.now

  cache_control :public, :must_revalidate, :max_age => 60

  expires 300, :public, :must_revalidate

  expire_cache_control # Assign browser cache-related headers to expire all cache

The controller class defines an inheritable 'content_type' class attribute which is used as a default.

Request Flow

Halt, Error, Redirect

Actions need sometimes be interrupted mid-flow. To handle these use-cases, Gin relies on the Sinatra-like halt, error, and redirect methods. These methods are available from Gin::Controller actions, filters, and error handlers.

  class UserController < Gin::Controller
    before_filter :logged_in do
      redirect "/session/new" unless logged_in?
    end

    before_filter :admin, :except => [:show, :index] do
      error 401, "You are not authorized to view this page" unless app_user.admin?
    end

    def destroy id
      halt 404, "Could not find user" unless @user = User.first(id)
      @user.destroy!
      "User deleted!"
    end
  end

Correct uses of request flow methods:

  halt 404
  halt 404, "body content"
  halt 404, {'Content-Type' => 'text/plain'}, "body content"

  error 404
  error 404, "body content"

  redirect "/location/uri"
  redirect "/location/uri", 301, "body content"

Other Flow Methods

Gin::Controller defines two more flow-altering methods for specific uses. The 'etag' method which halts the request and returns a 304 for matched If-Modified and If-None-Modified headers, and the 'send_file' method which sends back the contents of the given file path.

  class NewsController < AppController
    def show id
      @news = News.first(id)
      etag "news-#{@news.id}-#{@news.revision}"  # Halts and returns 304 if already viewed
      @news.to_html
    end

    def download_pdf id
      @path = News.pdf_path(id)
      send_file @path
    end
  end

Correct uses of 'etag' and 'send_file' methods:

  etag "value"
  etag "value", :weak
  etag "value", :type => :weak, :new_resource => true

  send_file "/path/to/file.pdf"
  send_file "/path/to/file", :type          => "application/pdf",
                             :disposition   => 'attachment',
                             :filename      => "report.pdf",
                             :last_modified => Time.now

Filters

Gin filters are defined in the controller and behave much like the Rails filters. They can be defined by directly passing a block to 'before_filter' and 'after_filter' class methods, or detached from the filter chain with the 'filter' class method.

Filters are run in the context of the controller instance.

  class AppController < Gin::Controller
    before_filter :logged_in # more filters and options can be passed as arguments

    filter :logged_in do
      redirect "/session/new" unless session_user
    end
  end

  # Equivalent, shorter version
  class AppController < Gin::Controller
    before_filter :logged_in do
      redirect "/session/new" unless session_user
    end
  end

Filters are inherited and can be skipped. All filter chaining methods support the :only and :except keys.

  class SessionController < AppController
    skip_before_filter :logged_in, :only => :create
  end

Error Handlers

Error handlers, like filters, are defined as blocks at the controller class level and inherited. They handle both uncaught exceptions and special HTTP response status codes.

  class SessionController < AppController
    # Handle status 401 and BadLogin errors the same way.
    error 401, BadLogin do |err|
      status 401
      body "You are not authorized to see this!"
    end
  end

Error handlers support exception inheritance so it's easy to catch a whole class (or all) exceptions.

  error do |err|
   # Handle all exceptions and status codes that haven't already been caught
  end

  error Exception do |err|
    # Handle all exceptions that haven't already been caught
  end

  error SocketError do |err|
    # SocketErrors get their own handler and won't trigger the Exception handler
  end

Because of common cases where code needs to run on all exceptions (like Exception logging), Gin::Controller provides an 'all_errors' method as a callback when exceptions are raised. This method gets run after error handlers are run, and is not triggered by special status codes.

  all_errors do |err|
    log_exception err
  end

Configuration

Gin implements a per-environment config files loading mechanism. Files must be YAML and in the app's 'config_dir'. Config files are defined per-feature with definitions for defaults, and multiple environments. The 'default' config is used by Gin if there is no match for the current environment name.

  # app/config/memcache.yml
  default: &default
    host: http://memcache.example.com
    connections: 5

  development: &dev
    host: localhost:123321
    connections: 1

  test: *dev

  staging:
    host: http://stage-memcache.example.com

  production: *default
  # app/my_app.rb
  MyApp.environment "staging"
  MyApp.config['memcache']
  #=> {'host' => 'http://stage-memcache.example.com', 'connections' => 5}

A specific value in the config can be accessed through and easy query String. If the query path is invalid, will return nil.

  @app.config['memcache.host']
  #=> "http://stage-memcache.example.com"
  @app.config['memcache.invalid.path.to.key']
  #=> nil

Configs can also be setup to auto-reload after a set amount of time. Reloading will only happen if the config attached to a given file is requested after the TTL period has expired.

  MyApp.config_reload 300   # seconds

  @app = MyApp.new
  @app.config['memcache.connections']
  #=> 5

  # app/config/memcache.yml gets updated so connections are increased
  sleep 300

  @app.config['memcache.connections']
  #=> 10

Test Helpers

Testing Gin applications is easy. The gin/test file provides helper and assertion methods for testing anything from views rendered to specific data points returned by an API-type endpoint.

Requiring gin/test will create a TestHelper module for each Gin::App subclass. These modules should be included in a Test::Unit::TestCase class.

  require 'gin/test'

  class TestMyApp < Test::Unit::TestCase
    include MyApp::TestHelper

    def test_show_user
      get "/user/123.json"
      assert_response :success
      assert_data "/user/firstname", "Bob"
    end
  end

Request helper methods can also be targeted to a specific controller, making segregating functional tests easy.

  require 'gin/test'

  class TestMyApp < Test::Unit::TestCase
    include MyApp::TestHelper
    controller MyApp::UserController

    def test_show_user
      get :show, id: 123, format: 'json'
      assert_response :success
      assert_data "/user/firstname", "Bob"
    end
  end

Auto Reload

For sanity reasons, not everything is reloaded when autoreload is enabled. Gin will only know to reload files and constants if the originating require was made from the Gin::App definition. Gin::App defines its own 'require' method in development mode, which keeps track of which files and constants get loaded at require time.

  # This uses Object#require and will not get reloaded
  require 'json'

  class MyApp < Gin::App
    # This file and all its dependencies will get reloaded automatically
    require 'my_app/controllers/user'
  end

Auto-reload may be disabled from the class by passing false to the 'autoreload' method.

  class MyApp < Gin::App
    autoreload false
  end