Jellyfish
Pico web framework for building API-centric web applications
Install / Use
/learn @godfat/JellyfishREADME
Jellyfish 
by Lin Jen-Shin (godfat)

LINKS:
DESCRIPTION:
Pico web framework for building API-centric web applications. For Rack applications or Rack middleware. Around 250 lines of code.
Check jellyfish-contrib for extra extensions.
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
dupincall - 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 (checkout jellyfish-contrib)
WHY?
Because Sinatra is too complex and inconsistent for me.
REQUIREMENTS:
- Tested with MRI (official CRuby) and JRuby.
INSTALLATION:
gem install jellyfish
SYNOPSIS:
You could also take a look at config.ru as an example.
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
<!---
GET /
[200,
{'content-length' => '12', 'content-type' => 'text/plain'},
["Jelly Kelly\n"]]
-->
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
<!---
GET /123
[200,
{'content-length' => '11', 'content-type' => 'text/plain'},
["Jelly #123\n"]]
-->
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
<!---
GET /hctam
[200,
{'content-length' => '5', 'content-type' => 'text/plain'},
["true\n"]]
-->
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
<!---
POST /
[201,
{'content-length' => '18', 'content-type' => 'text/plain',
'X-Jellyfish-Life' => '100', 'X-Jellyfish-Mana' => '200'},
["Jellyfish 100/200\n"]]
-->
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
<!---
GET /lookup host
body = File.read("#{File.dirname(
File.expand_path(__FILE__))}/../lib/jellyfish/public/302.html").
gsub('VAR_URL', 'http://host/')
[302,
{'content-length' => body.bytesize.to_s, 'content-type' => 'text/html',
'location' => 'http://host/'},
[body]]
-->
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
<!---
GET /crash
body = File.read("#{File.dirname(
File.expand_path(__FILE__))}/../lib/jellyfish/public/500.html")
[500,
{'content-length' => body.bytesize.to_s, 'content-type' => 'text/html'},
[body]]
-->
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
<!---
GET /yell
body = case RUBY_ENGINE
when 'jruby'
"No one hears you: (eval):9:in `block in Tank'\n"
when 'rbx'
"No one hears you: core/zed.rb:1370:in `yell (method_missing)'\n"
else
"No one hears you: (eval):9:in `block in <class:Tank>'\n"
end
[403,
{'content-length' => body.bytesize.to_s, 'content-type' => 'text/plain'},
[body]]
-->
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
<!---
GET /
[404,
{'content-length' => '18', 'content-type' => 'text/plain'},
["You found nothing."]]
-->
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
<!---
GET /
[404,
{'content-length' => '18', 'content-type' => 'text/plain'},
["You found nothing."]]
-->
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
<!---
GET /report?name=godfat
[200,
{'content-length' => '20', 'content-type' => 'text/plain'},
["Your name is godfat\n"]]
-->
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
<!---
GET /report
[200,
{'content-length' => '3', 'content-type' => 'text/plain'},
["OK\n"]]
-->
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
<!---
GET /status
[200,
{'content-length' => '6', 'content-type' => 'text/plain'},
["30\u{2103}\n"]]
-->
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
<!---
GET /status
[200,
{'content-length' => '6', 'content-type' => 'text/plain'},
["30\u{2103}\n"]]
-->
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
<!---
GET /123
[200,
{'content-length' => '13', 'content-type' => 'text/plain'},
["Jelly jumps.\n"]]
-->
Extension: Jellyfish::Builder, a faster Rack::Builder and Rack::URLMap
Default Rack::Builder and Rack::URLMap is routing via linear search,
which could be very slow with a large number of routes. We could use
Jellyfish::Builder in this case because it would compile the routes
into a regular expression, it would be matching much faster than
linear search.
Note that Jellyfish::Builder is not a complete compatible implementation.
The followings are intentional:
-
There's no
Jellyfish::Builder.callbecause it doesn't make sense in my opinion. Always useJellyfish::Builder.appinstead. -
There's no
Jellyfish::Builder.parse_fileandJellyfish::Builder.new_from_stringbecause Rack servers are not going to useJellyfish::Builderto parseconfig.ruat this point. We could provide this if there's a need. -
Jellyfish::URLMapdoes not modifyenv, and it would call the app with another instance of Hash. Mutating data is a bad idea. -
All other tests passed the same test suites for
Rack::Builderand `Jellyfish::URLMa
