Jellyfish
by Lin Jen-Shin (godfat)
LINKS:
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
incall
- 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:
- 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.