# Jellyfish[![Build Status](https://secure.travis-ci.org/godfat/jellyfish.png?branch=master)](http://travis-ci.org/godfat/jellyfish) by Lin Jen-Shin ([godfat](http://godfat.org)) ![logo](https://github.com/godfat/jellyfish/raw/master/jellyfish.png) ## LINKS: * [github](https://github.com/godfat/jellyfish) * [rubygems](https://rubygems.org/gems/jellyfish) * [rdoc](http://rdoc.info/github/godfat/jellyfish) ## 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 Rack functionalities, again * Learn regular expression for routes instead of custom syntax * Embrace simplicity over convenience * Don't make things complicated only for _some_ convenience, but _great_ convenience, or simply stay simple for simplicity. * More features are added as extensions ## FEATURES: * Minimal * Simple * Modular * No templates (You could use [tilt](https://github.com/rtomayko/tilt)) * No ORM * No `dup` in `call` * Regular expression routes, e.g. `get %r{^/(?\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) 1.9.3, 2.0.0, Rubinius and JRuby. ## INSTALLATION: gem install jellyfish ## SYNOPSIS: ### Hello Jellyfish, your lovely config.ru ``` ruby 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 ``` ruby require 'jellyfish' class Tank include Jellyfish get %r{^/(?\d+)$} do |match| "Jelly ##{match[:id]}\n" end end use Rack::ContentLength use Rack::ContentType, 'text/plain' run Tank.new ``` ### Custom matcher routes ``` ruby 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 ``` ruby 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 ``` ruby 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 ``` ruby 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 ``` ruby 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 ``` ### Access Rack::Request and params ``` ruby 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 ``` ruby 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. ``` ruby 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. ``` ruby 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 ``` ### Extension: MultiActions (Filters) ``` ruby 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) ``` ruby require 'jellyfish' class Tank include Jellyfish controller_include Jellyfish::NormalizedParams get %r{^/(?\d+)$} do "Jelly ##{params[:id]}\n" end end use Rack::ContentLength use Rack::ContentType, 'text/plain' run Tank.new ``` ### Extension: NormalizedPath (with unescaping) ``` ruby 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 ``` ruby require 'jellyfish' class Tank include Jellyfish controller_include Jellyfish::Sinatra get do # wildcard before filter @state = 'jumps' end get %r{^/(?\d+)$} do "Jelly ##{params[:id]} #{@state}.\n" end end use Rack::ContentLength use Rack::ContentType, 'text/plain' run Tank.new ``` ### Extension: NewRelic ``` ruby 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. ``` ruby 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{^/(?\d+)$} do "Jelly ##{params[:id]} #{@state}.\n" end end use Rack::ContentLength use Rack::ContentType, 'text/plain' run Tank.new ``` ### Jellyfish as a middleware ``` ruby 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 ``` ruby 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 ``` ### Default headers as a middleware ``` ruby require 'jellyfish' class Heater include Jellyfish get '/status' do status, headers, body = jellyfish.app.call(env) self.status status self.headers({'X-Temperature' => "30\u{2103}"}.merge(headers)) self.body body end end class Tank include Jellyfish get '/status' do headers_merge('X-Temperature' => "35\u{2103}") "\n" end end use Rack::ContentLength use Rack::ContentType, 'text/plain' use Heater run Tank.new ``` ### Simple before action as a middleware ``` ruby require 'jellyfish' class Heater include Jellyfish get '/status' do env['temperature'] = 30 forward 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 ``` ruby 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 ``` ruby 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.new do use Rack::ContentLength use Rack::ContentType, 'text/plain' use Heater run Tank.new end run HugeTank ``` ### Raise exceptions ``` ruby require 'jellyfish' class Protector include Jellyfish handle Exception 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: ``` ruby 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 ``` ruby 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 ``` ## CONTRIBUTORS: * Lin Jen-Shin (@godfat) ## LICENSE: Apache License 2.0 Copyright (c) 2012-2013, 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 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.