Jellyfish Build Status Coverage Status

by Lin Jen-Shin (godfat)

logo

DESCRIPTION:

Pico web framework for building API-centric web applications. For Rack applications or Rack middlewares. Around 250 lines of code.

DESIGN:

  • Learn the HTTP way instead of using some pointless helpers.
  • Learn the Rack way instead of wrapping around Rack functionalities.
  • Learn regular expression for routes instead of custom syntax.
  • Embrace simplicity over convenience.
  • Don't make things complicated only for some convenience, but for great convenience, or simply stay simple for simplicity.
  • More features are added as extensions.
  • Consider use rack-protection if you're not only building an API server.
  • Consider use websocket_parser if you're trying to use WebSocket. Please check example below.

FEATURES:

  • Minimal
  • Simple
  • Modular
  • No templates (You could use tilt)
  • No ORM (You could use sequel)
  • No dup in call
  • Regular expression routes, e.g. get %r{^/(?<id>\d+)$}
  • String routes, e.g. get '/'
  • Custom routes, e.g. get Matcher.new
  • Build for either Rack applications or Rack middleware
  • Include extensions for more features (There's a Sinatra extension)

WHY?

Because Sinatra is too complex and inconsistent for me.

REQUIREMENTS:

  • Tested with MRI (official CRuby), Rubinius and JRuby.

INSTALLATION:

gem install jellyfish

SYNOPSIS:

You could also take a look at config.ru as an example, which also uses Swagger to generate API documentation.

Hello Jellyfish, your lovely config.ru

require 'jellyfish'
class Tank
  include Jellyfish
  get '/' do
    "Jelly Kelly\n"
  end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new

Regular expression routes

require 'jellyfish'
class Tank
  include Jellyfish
  get %r{^/(?<id>\d+)$} do |match|
    "Jelly ##{match[:id]}\n"
  end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new

Custom matcher routes

require 'jellyfish'
class Tank
  include Jellyfish
  class Matcher
    def match path
      path.reverse == 'match/'
    end
  end
  get Matcher.new do |match|
    "#{match}\n"
  end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new

Different HTTP status and custom headers

require 'jellyfish'
class Tank
  include Jellyfish
  post '/' do
    headers       'X-Jellyfish-Life' => '100'
    headers_merge 'X-Jellyfish-Mana' => '200'
    body "Jellyfish 100/200\n"
    status 201
    'return is ignored if body has already been set'
  end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new

Redirect helper

require 'jellyfish'
class Tank
  include Jellyfish
  get '/lookup' do
    found "#{env['rack.url_scheme']}://#{env['HTTP_HOST']}/"
  end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new

Crash-proof

require 'jellyfish'
class Tank
  include Jellyfish
  get '/crash' do
    raise 'crash'
  end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new

Custom error handler

require 'jellyfish'
class Tank
  include Jellyfish
  handle NameError do |e|
    status 403
    "No one hears you: #{e.backtrace.first}\n"
  end
  get '/yell' do
    yell
  end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new

Custom error 404 handler

require 'jellyfish'
class Tank
  include Jellyfish
  handle Jellyfish::NotFound do |e|
    status 404
    "You found nothing."
  end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new

Custom error handler for multiple errors

require 'jellyfish'
class Tank
  include Jellyfish
  handle Jellyfish::NotFound, NameError do |e|
    status 404
    "You found nothing."
  end
  get '/yell' do
    yell
  end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new

Access Rack::Request and params

require 'jellyfish'
class Tank
  include Jellyfish
  get '/report' do
    "Your name is #{request.params['name']}\n"
  end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new

Re-dispatch the request with modified env

require 'jellyfish'
class Tank
  include Jellyfish
  get '/report' do
    status, headers, body = jellyfish.call(env.merge('PATH_INFO' => '/info'))
    self.status  status
    self.headers headers
    self.body    body
  end
  get('/info'){ "OK\n" }
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new

Include custom helper in built-in controller

Basically it's the same as defining a custom controller and then include the helper. This is merely a short hand. See next section for defining a custom controller.

require 'jellyfish'
class Heater
  include Jellyfish
  get '/status' do
    temperature
  end

  module Helper
    def temperature
      "30\u{2103}\n"
    end
  end
  controller_include Helper
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Heater.new

Define custom controller manually

This is effectively the same as defining a helper module as above and include it, but more flexible and extensible.

require 'jellyfish'
class Heater
  include Jellyfish
  get '/status' do
    temperature
  end

  class Controller < Jellyfish::Controller
    def temperature
      "30\u{2103}\n"
    end
  end
  controller Controller
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Heater.new

Override dispatch for processing before action

We don't have before action built-in, but we could override dispatch in the controller to do the same thing. CAVEAT: Remember to call super.

require 'jellyfish'
class Tank
  include Jellyfish
  controller_include Module.new{
    def dispatch
      @state = 'jumps'
      super
    end
  }

  get do
    "Jelly #{@state}.\n"
  end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new

Extension: MultiActions (Filters)

require 'jellyfish'
class Tank
  include Jellyfish
  controller_include Jellyfish::MultiActions

  get do # wildcard before filter
    @state = 'jumps'
  end
  get do
    "Jelly #{@state}.\n"
  end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new

Extension: NormalizedParams (with force_encoding)

require 'jellyfish'
class Tank
  include Jellyfish
  controller_include Jellyfish::NormalizedParams

  get %r{^/(?<id>\d+)$} do
    "Jelly ##{params[:id]}\n"
  end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new

Extension: NormalizedPath (with unescaping)

require 'jellyfish'
class Tank
  include Jellyfish
  controller_include Jellyfish::NormalizedPath

  get "/\u{56e7}" do
    "#{env['PATH_INFO']}=#{path_info}\n"
  end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new

Extension: Sinatra flavoured controller

It's an extension collection contains:

  • MultiActions
  • NormalizedParams
  • NormalizedPath
require 'jellyfish'
class Tank
  include Jellyfish
  controller_include Jellyfish::Sinatra

  get do # wildcard before filter
    @state = 'jumps'
  end
  get %r{^/(?<id>\d+)$} do
    "Jelly ##{params[:id]} #{@state}.\n"
  end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new

Extension: NewRelic

require 'jellyfish'
class Tank
  include Jellyfish
  controller_include Jellyfish::NewRelic

  get '/' do
    "OK\n"
  end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
require 'cgi' # newrelic dev mode needs this and it won't require it itself
require 'new_relic/rack/developer_mode'
use NewRelic::Rack::DeveloperMode # GET /newrelic to read stats
run Tank.new
NewRelic::Agent.manual_start(:developer_mode => true)

Extension: Using multiple extensions with custom controller

This is effectively the same as using Jellyfish::Sinatra extension. Note that the controller should be assigned lastly in order to include modules remembered in controller_include.

require 'jellyfish'
class Tank
  include Jellyfish
  class MyController < Jellyfish::Controller
    include Jellyfish::MultiActions
  end
  controller_include NormalizedParams, NormalizedPath
  controller MyController

  get do # wildcard before filter
    @state = 'jumps'
  end
  get %r{^/(?<id>\d+)$} do
    "Jelly ##{params[:id]} #{@state}.\n"
  end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new

Jellyfish as a middleware

If the Jellyfish middleware cannot find a corresponding action, it would then forward the request to the lower application. We call this cascade.

require 'jellyfish'
class Heater
  include Jellyfish
  get '/status' do
    "30\u{2103}\n"
  end
end

class Tank
  include Jellyfish
  get '/' do
    "Jelly Kelly\n"
  end
end

use Rack::ContentLength
use Rack::ContentType, 'text/plain'
use Heater
run Tank.new

Modify response as a middleware

We could also explicitly call the lower app. This would give us more flexibility than simply forwarding it.

require 'jellyfish'
class Heater
  include Jellyfish
  get '/status' do
    status, headers, body = jellyfish.app.call(env)
    self.status  status
    self.headers headers
    self.body    body
    headers_merge('X-Temperature' => "30\u{2103}")
  end
end

class Tank
  include Jellyfish
  get '/status' do
    "See header X-Temperature\n"
  end
end

use Rack::ContentLength
use Rack::ContentType, 'text/plain'
use Heater
run Tank.new

Override cascade for customized forwarding

We could also override cascade in order to craft custom response when forwarding is happening. Note that whenever this forwarding is happening, Jellyfish won't try to merge the headers from dispatch method, because in this case Jellyfish is served as a pure proxy. As result we need to explicitly merge the headers if we really want.

require 'jellyfish'
class Heater
  include Jellyfish
  controller_include Module.new{
    def dispatch
      headers_merge('X-Temperature' => "35\u{2103}")
      super
    end

    def cascade
      status, headers, body = jellyfish.app.call(env)
      halt [status, headers_merge(headers), body]
    end
  }
end

class Tank
  include Jellyfish
  get '/status' do
    "\n"
  end
end

use Rack::ContentLength
use Rack::ContentType, 'text/plain'
use Heater
run Tank.new

Simple before action as a middleware

require 'jellyfish'
class Heater
  include Jellyfish
  get '/status' do
    env['temperature'] = 30
    cascade
  end
end

class Tank
  include Jellyfish
  get '/status' do
    "#{env['temperature']}\u{2103}\n"
  end
end

use Rack::ContentLength
use Rack::ContentType, 'text/plain'
use Heater
run Tank.new

Halt in before action

require 'jellyfish'
class Tank
  include Jellyfish
  controller_include Jellyfish::MultiActions

  get do # wildcard before filter
    body "Done!\n"
    halt
  end
  get '/' do
    "Never reach.\n"
  end
end

use Rack::ContentLength
use Rack::ContentType, 'text/plain'
run Tank.new

One huge tank

require 'jellyfish'
class Heater
  include Jellyfish
  get '/status' do
    "30\u{2103}\n"
  end
end

class Tank
  include Jellyfish
  get '/' do
    "Jelly Kelly\n"
  end
end

HugeTank = Rack::Builder.app do
  use Rack::ContentLength
  use Rack::ContentType, 'text/plain'
  use Heater
  run Tank.new
end

run HugeTank

Raise exceptions

require 'jellyfish'
class Protector
  include Jellyfish
  handle StandardError do |e|
    "Protected: #{e}\n"
  end
end

class Tank
  include Jellyfish
  handle_exceptions false # default is true, setting false here would make
                          # the outside Protector handle the exception
  get '/' do
    raise "Oops, tank broken"
  end
end

use Rack::ContentLength
use Rack::ContentType, 'text/plain'
use Protector
run Tank.new

Chunked transfer encoding (streaming) with Jellyfish::ChunkedBody

You would need a proper server setup. Here's an example with Rainbows and fibers:

class Tank
  include Jellyfish
  get '/chunked' do
    ChunkedBody.new{ |out|
      (0..4).each{ |i| out.call("#{i}\n") }
    }
  end
end
use Rack::Chunked
use Rack::ContentType, 'text/plain'
run Tank.new

Chunked transfer encoding (streaming) with custom body

class Tank
  include Jellyfish
  class Body
    def each
      (0..4).each{ |i| yield "#{i}\n" }
    end
  end
  get '/chunked' do
    Body.new
  end
end
use Rack::Chunked
use Rack::ContentType, 'text/plain'
run Tank.new

Using WebSocket

Note that this only works for Rack servers which support hijack. You're better off with a threaded server such as Rainbows! with thread based concurrency model, or Puma.

Event-driven based server is a whole different story though. Since EventMachine is basically dead, we could see if there would be a Celluloid-IO based web server production ready in the future, so that we could take the advantage of event based approach.

class Tank
  include Jellyfish
  controller_include Jellyfish::WebSocket
  get '/echo' do
    switch_protocol do |msg|
      ws_write(msg)
    end
    ws_write('Hi!')
    ws_start
  end
end
run Tank.new

Use Swagger to generate API documentation

For a complete example, checkout config.ru.

require 'jellyfish'
class Tank
  include Jellyfish
  get %r{^/(?<id>\d+)$}, :notes => 'This is an API note' do |match|
    "Jelly ##{match[:id]}\n"
  end
end
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
map '/swagger' do
  run Jellyfish::Swagger.new('', Tank)
end
run Tank.new

CONTRIBUTORS:

  • Jason R. Clark (@jasonrclark)
  • Lin Jen-Shin (@godfat)

LICENSE:

Apache License 2.0

Copyright (c) 2012-2014, Lin Jen-Shin (godfat)

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.