SkillAgentSearch skills...

Jellyfish

Pico web framework for building API-centric web applications

Install / Use

/learn @godfat/Jellyfish
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Jellyfish Pipeline status

by Lin Jen-Shin (godfat)

logo

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 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 (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.call because it doesn't make sense in my opinion. Always use Jellyfish::Builder.app instead.

  • There's no Jellyfish::Builder.parse_file and Jellyfish::Builder.new_from_string because Rack servers are not going to use Jellyfish::Builder to parse config.ru at this point. We could provide this if there's a need.

  • Jellyfish::URLMap does not modify env, 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::Builder and `Jellyfish::URLMa

View on GitHub
GitHub Stars54
CategoryDevelopment
Updated9mo ago
Forks5

Languages

Ruby

Security Score

87/100

Audited on Jun 26, 2025

No findings