Modular webserver interface for mruby
Ruby
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
mrblib
test
.appveyor.yml
.codebeatsettings
.gitignore
.hound.yml
.rubocop.yml
.travis.yml
LICENSE
README.md
Rakefile
build_config.rb
mrbgem.rake

README.md

Shelf, a modular webserver interface for mruby
Build Status Build status codebeat badge

Inspired by Rack, empowers mruby, a work in progress!

Rack provides a minimal, modular, and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call.

The exact details of this are described in the Rack specification, which all Rack applications should conform to.

-- https://github.com/rack/rack

Shelf::Builder.app do
  run ->(env) { [200, {}, ['A barebones shelf app']] }
end

Installation

Add the line below to your build_config.rb:

MRuby::Build.new do |conf|
  # ... (snip) ...
  conf.gem 'mruby-shelf'
end

Or add this line to your aplication's mrbgem.rake:

MRuby::Gem::Specification.new('your-mrbgem') do |spec|
  # ... (snip) ...
  spec.add_dependency 'mruby-shelf'
end

Builder

The Rack::Builder DSL is compatible with Shelf::Builder. Shelf uses mruby-r3 for the path dispatching to add some nice extras.

app = Shelf::Builder.app do
  run ->(env) { [200, { 'content-type' => 'text/plain' }, ['A barebones shelf app']] }
end

app.call('REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/')
# => [200, { 'content-type' => 'text/plain' }, ['A barebones shelf app']]

app.call('REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/info')
# => [404, { 'content-type' => 'text/plain', 'X-Cascade' => 'pass' }, ['Not Found']]

Using middleware layers is dead simple:

class NoContent
  def initialize(app)
    @app = app
  end

  def call(env)
    [204, @app.call(env)[1], []]
  end
end

app = Shelf::Builder.app do
  use NoContent
  run ->(env) { [200, { ... }, ['A barebones shelf app']] }
end

app.call('REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/')
# => [204, { ... }, []]

Mounted routes may contain slugs and can be restricted to a certain HTTP method:

app = Shelf::Builder.app do
  map('/users/{id}', :GET) { run ->(env) { [200, { ... }, [env['shelf.request.query_hash'][:id]]] } }
end

app.call('REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/users/1')
# => [200, { ... }, ['1']]

app.call('REQUEST_METHOD' => 'PUT', 'PATH_INFO' => '/users/1')
# => [405, { ... }, ['Method Not Allowed']]

Routes can store any kind of additional data:

app = Shelf::Builder.app do
  get('data', [Object.new]) { run ->(env) { [200, { ... }, env['shelf.r3.data']] } }
end

app.call('REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/data')
# => [200, { ... }, ['#<Object:0x007fd5739dfe40>']]

Handler

The Rack::Handler class is mostly compatible with Shelf::Handler except that it takes the handler class instead of the path string.

Shelf::Handler.register 'h2o', H2O::Shelf::Handler

Per default Shelf uses its built-in handler for mruby-simplehttpserver:

Shelf::Handler.default
# => Shelf::Handler::SimpleHttpServer

Howver its possible to customize that:

ENV['SHELF_HANDLER'] = 'h2o'

Server

The Rack::Server API is mostly compatible with Shelf::Server except that there's no config.ru file, built-in opt parser. Only the main options (:app, :port, :host, ...) are supported. Also note that :host and :port are written downcase!

Shelf::Server.start(
  app: ->(e) {
    [200, { 'Content-Type' => 'text/html' }, ['hello world']]
  },
  server: 'simplehttpserver'
)

The default middleware stack can be extended per environment:

Shelf::Server.middleware[:production] << MyCustomMiddleware

Middleware

Shelf comes with some useful middlewares. These can be defined by app or by environment.

  • ContentLength

    app = Shelf::Builder.app do
      use Shelf::ContentLength
      run ->(env) { [200, {}, ['A barebones shelf app']] }
    end
    
    app.call('REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/')
    # => [200, { 'Content-Length' => 21 }, ['A barebones shelf app']]
  • ContentType

    app = Shelf::Builder.app do
      use Shelf::ContentLength
      use Shelf::ContentType, 'text/plain'
      run ->(env) { [200, {}, ['A barebones shelf app']] }
    end
    
    app.call('REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/')
    # => [200, { 'Content-Length' => 21, 'Content-Type' => 'text/plain' }, ['A barebones shelf app']]
  • QueryParser

    app = Shelf::Builder.app do
      map('/users/{id}') do
        use Shelf::QueryParser
        run ->(env) { [200, env['shelf.request.query_hash'], []] }
      end
    end
    
    app.call('REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/users/1', 'QUERY_STRING' => 'field=age&field=name')
    # => [200, { 'id' => '1', 'field' => ['age', 'name'] }, []]
  • Head

    app = Shelf::Builder.app do
      use Shelf::Head
      run ->(env) { [200, {}, ['A barebones shelf app']] }
    end
    
    app.call('REQUEST_METHOD' => 'HEAD', 'PATH_INFO' => '/')
    # => [200, { 'Content-Length' => 21 }, []]
  • Static

    app = Shelf::Builder.app do
      use Shelf::Static, urls: { '/' => 'index.html' }, root: 'public'
      run ->(env) { [200, {}, ['A barebones shelf app']] }
    end
    
    app.call('REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/')
    # => [200, { 'Content-Length' => xxx, 'Content-Type' => 'text/html; charset=utf-8' }, ['<html>...</html>']]
  • Logger

    app = Shelf::Builder.app do
      use Shelf::Logger, Logger::INFO
      run ->(env) { [200, {}, [Log-Level: "#{env['shelf.logger'].level}"] }
    end
    
    app.call('REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/')
    # => [200, {}, ['Log-Level: 1']]
    • Writes to env[SHELF_ERRORS] which is $stderr by default
    • Requires mruby-logger
  • CommonLogger

    app = Shelf::Builder.app do
      use Shelf::CommonLogger, Logger.new
      run ->(env) { [200, {}, ['A barebones shelf app']] }
    end
    
    app.call('REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/index.html')
    # => 127.0.0.1 - [23/05/2017:18:03:36 +0200] "GET /index.html HTTP/1.1" 200 2326
  • CatchError

    app = Shelf::Builder.app do
      use Shelf::CatchError
      run ->(env) { undef_method_call }
    end
    
    app.call('REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/')
    # => [500, { 'Content-Length' => 21, 'Content-Type' => 'text/plain' }, ['Internal Server Error']]
    • Requires mruby-io
    • Writes all expection traces to env[SHELF_ERRORS]
    • Response body contains the stack trace under development mode
  • Deflater

    app = Shelf::Builder.app do
      use Shelf::Deflater
      run ->(env) { [200, {}, ['A barebones shelf app']] }
    end
    
    app.call('REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/', 'Accept-Encoding' => 'gzip')
    # => [200, { 'Content-Encoding' => 'gzip', ... }, ['...']]

Development

Clone the repo:

$ git clone https://github.com/katzer/mruby-shelf.git && cd mruby-shelf/

Compile the source:

$ rake compile

Run the tests:

$ rake test

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/katzer/mruby-shelf.

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Authors

  • Sebastián Katzer, Fa. appPlant GmbH

License

The mgem is available as open source under the terms of the MIT License.

Made with 😋 from Leipzig

© 2017 appPlant GmbH