Git Product home page Git Product logo

roda's Introduction

A routing tree web toolkit, designed for building fast and maintainable web applications in Ruby.

Table of contents

Installation

$ gem install roda

Resources

Website

roda.jeremyevans.net

Source

github.com/jeremyevans/roda

Bugs

github.com/jeremyevans/roda/issues

Discussion Forum (GitHub Discussions)

github.com/jeremyevans/roda/discussions

Alternate Discussion Forum (Google Group)

groups.google.com/group/ruby-roda

Goals

  • Simplicity

  • Reliability

  • Extensibility

  • Performance

Simplicity

Roda is designed to be simple, both internally and externally. It uses a routing tree to enable you to write simpler and DRYer code.

Reliability

Roda supports and encourages immutability. Roda apps are designed to be frozen in production, which eliminates possible thread safety issues. Additionally, Roda limits the instance variables, constants, and methods that it uses, so that they do not conflict with the ones you use for your application.

Extensibility

Roda is built completely out of plugins, which makes it very extensible. You can override any part of Roda and call super to get the default behavior.

Performance

Roda has low per-request overhead, and the use of a routing tree and intelligent caching of internal datastructures makes it significantly faster than other popular ruby web frameworks.

Usage

Here’s a simple application, showing how the routing tree works:

# cat config.ru
require "roda"

class App < Roda
  route do |r|
    # GET / request
    r.root do
      r.redirect "/hello"
    end

    # /hello branch
    r.on "hello" do
      # Set variable for all routes in /hello branch
      @greeting = 'Hello'

      # GET /hello/world request
      r.get "world" do
        "#{@greeting} world!"
      end

      # /hello request
      r.is do
        # GET /hello request
        r.get do
          "#{@greeting}!"
        end

        # POST /hello request
        r.post do
          puts "Someone said #{@greeting}!"
          r.redirect
        end
      end
    end
  end
end

run App.freeze.app

Here’s a breakdown of what is going on in the block above:

The route block is called whenever a new request comes in. It is yielded an instance of a subclass of Rack::Request with some additional methods for matching routes. By convention, this argument should be named r.

The primary way routes are matched in Roda is by calling r.on, r.is, r.root, r.get, or r.post. Each of these “routing methods” takes a “match block”.

Each routing method takes each of the arguments (called matchers) that are given and tries to match it to the current request. If the method is able to match all of the arguments, it yields to the match block; otherwise, the block is skipped and execution continues.

  • r.on matches if all of the arguments match.

  • r.is matches if all of the arguments match and there are no further entries in the path after matching.

  • r.get matches any GET request when called without arguments.

  • r.get (when called with any arguments) matches only if the current request is a GET request and there are no further entries in the path after matching.

  • r.root only matches a GET request where the current path is /.

If a routing method matches and control is yielded to the match block, whenever the match block returns, Roda will return the Rack response array (containing status, headers, and body) to the caller.

If the match block returns a string and the response body hasn’t already been written to, the block return value will be interpreted as the body for the response. If none of the routing methods match and the route block returns a string, it will be interpreted as the body for the response.

r.redirect immediately returns the response, allowing for code such as r.redirect(path) if some_condition. If r.redirect is called without arguments and the current request method is not GET, it redirects to the current path.

The .freeze.app at the end is optional. Freezing the app makes modifying app-level settings raise an error, alerting you to possible thread-safety issues in your application. It is recommended to freeze the app in production and during testing. The .app is an optimization, which saves a few method calls for every request.

Running the Application

Running a Roda application is similar to running any other rack-based application that uses a config.ru file. You can start a basic server using rackup, puma, unicorn, passenger, or any other webserver that can handle config.ru files:

$ rackup

The Routing Tree

Roda is called a routing tree web toolkit because the way most sites are structured, routing takes the form of a tree (based on the URL structure of the site). In general:

  • r.on is used to split the tree into different branches.

  • r.is finalizes the routing path.

  • r.get and r.post handle specific request methods.

So, a simple routing tree might look something like this:

r.on "a" do           # /a branch
  r.on "b" do         # /a/b branch
    r.is "c" do       # /a/b/c request
      r.get do end    # GET  /a/b/c request
      r.post do end   # POST /a/b/c request
    end
    r.get "d" do end  # GET  /a/b/d request
    r.post "e" do end # POST /a/b/e request
  end
end

It’s also possible to handle the same requests, but structure the routing tree by first branching on the request method:

r.get do              # GET
  r.on "a" do         # GET /a branch
    r.on "b" do       # GET /a/b branch
      r.is "c" do end # GET /a/b/c request
      r.is "d" do end # GET /a/b/d request
    end
  end
end

r.post do             # POST
  r.on "a" do         # POST /a branch
    r.on "b" do       # POST /a/b branch
      r.is "c" do end # POST /a/b/c request
      r.is "e" do end # POST /a/b/e request
    end
  end
end

This allows you to easily separate your GET request handling from your POST request handling. If you only have a small number of POST request URLs and a large number of GET request URLs, this may make things easier.

However, routing first by the path and last by the request method is likely to lead to simpler and DRYer code. This is because you can act on the request at any point during the routing. For example, if all requests in the /a branch need access permission A and all requests in the /a/b branch need access permission B, you can easily handle this in the routing tree:

r.on "a" do           # /a branch
  check_perm(:A)  
  r.on "b" do         # /a/b branch
    check_perm(:B)  
    r.is "c" do       # /a/b/c request
      r.get do end    # GET  /a/b/c request
      r.post do end   # POST /a/b/c request
    end
    r.get "d" do end  # GET  /a/b/d request
    r.post "e" do end # POST /a/b/e request
  end
end

Being able to operate on the request at any point during the routing is one of the major advantages of Roda.

Matchers

Other than r.root, the routing methods all take arguments called matchers. If all of the matchers match, the routing method yields to the match block. Here’s an example showcasing how different matchers work:

class App < Roda
  route do |r|
    # GET /
    r.root do
      "Home"
    end

    # GET /about
    r.get "about" do
      "About"
    end

    # GET /post/2011/02/16/hello
    r.get "post", Integer, Integer, Integer, String do |year, month, day, slug|
      "#{year}-#{month}-#{day} #{slug}" #=> "2011-02-16 hello"
    end

    # GET /username/foobar branch
    r.on "username", String, method: :get do |username|
      user = User.find_by_username(username)

      # GET /username/foobar/posts
      r.is "posts" do
        # You can access user here, because the blocks are closures.
        "Total Posts: #{user.posts.size}" #=> "Total Posts: 6"
      end

      # GET /username/foobar/following
      r.is "following" do
        user.following.size.to_s #=> "1301"
      end
    end

    # /search?q=barbaz
    r.get "search" do
      "Searched for #{r.params['q']}" #=> "Searched for barbaz"
    end

    r.is "login" do
      # GET /login
      r.get do
        "Login"
      end

      # POST /login?user=foo&password=baz
      r.post do
        "#{r.params['user']}:#{r.params['password']}" #=> "foo:baz"
      end
    end
  end
end

Here’s a description of the matchers. Note that “segment”, as used here, means one part of the path preceded by a /. So, a path such as /foo/bar//baz has four segments: /foo, /bar, /, and /baz. The / here is considered the empty segment.

String

If a string does not contain a slash, it matches a single segment containing the text of the string, preceded by a slash.

""    # matches "/"
"foo" # matches "/foo"
"foo" # does not match "/food"

If a string contains any slashes, it matches one additional segment for each slash:

"foo/bar" # matches "/foo/bar"
"foo/bar" # does not match "/foo/bard"

Regexp

Regexps match one or more segments by looking for the pattern, preceded by a slash, and followed by a slash or the end of the path:

/foo\w+/ # matches "/foobar"
/foo\w+/ # does not match "/foo/bar"
/foo/i # matches "/foo", "/Foo/"
/foo/i # does not match "/food"

If any patterns are captured by the Regexp, they are yielded:

/foo\w+/   # matches "/foobar", yields nothing
/foo(\w+)/ # matches "/foobar", yields "bar"

Class

There are two classes that are supported as matchers, String and Integer.

String

matches any non-empty segment, yielding the segment except for the preceding slash

Integer

matches any segment of 0-9, returns matched values as integers

Using String and Integer is the recommended way to handle arbitrary segments

String # matches "/foo", yields "foo"
String # matches "/1", yields "1"
String # does not match "/"

Integer # does not match "/foo"
Integer # matches "/1", yields 1
Integer # does not match "/"

Symbol

Symbols match any nonempty segment, yielding the segment except for the preceding slash:

:id # matches "/foo" yields "foo"
:id # does not match "/"

Symbol matchers operate the same as the class String matcher, and is the historical way to do arbitrary segment matching. It is recommended to use the class String matcher in new code as it is a bit more intuitive.

Proc

Procs match unless they return false or nil:

proc{true}  # matches anything
proc{false} # does not match anything

Procs don’t capture anything by default, but they can do so if you add the captured text to r.captures.

Arrays

Arrays match when any of their elements match. If multiple matchers are given to r.on, they all must match (an AND condition). If an array of matchers is given, only one needs to match (an OR condition). Evaluation stops at the first matcher that matches.

Additionally, if the matched object is a String, the string is yielded. This makes it easy to handle multiple strings without a Regexp:

['page1', 'page2'] # matches "/page1", "/page2"
[]                 # does not match anything

Hash

Hashes allow easily calling specialized match methods on the request. The default registered matchers included with Roda are documented below. Some plugins add additional hash matchers, and the hash_matcher plugin allows for easily defining your own:

class App < Roda
  plugin :hash_matcher

  hash_matcher(:foo) do |v|
    # ...
  end

  route do |r|
    r.on foo: 'bar' do
      # ...
    end
  end
end

:all

The :all matcher matches if all of the entries in the given array match, so

r.on all: [String, String] do
  # ...
end

is the same as:

r.on String, String do
  # ...
end

The reason it also exists as a separate hash matcher is so you can use it inside an array matcher, so:

r.on ['foo', {all: ['foos', Integer]}] do
end

would match /foo and /foos/10, but not /foos.

:method

The :method matcher matches the method of the request. You can provide an array to specify multiple request methods and match on any of them:

{method: :post}             # matches POST
{method: ['post', 'patch']} # matches POST and PATCH

true

If true is given directly as a matcher, it always matches.

false, nil

If false or nil is given directly as a matcher, it doesn’t match anything.

Everything else

Everything else raises an error, unless support is specifically added for it (some plugins add support for additional matcher types).

Optional segments

There are multiple ways you can handle optional segments in Roda. For example, let’s say you want to accept both /items/123 and /items/123/456, with 123 being the item’s id, and 456 being some optional data.

The simplest way to handle this is by treating this as two separate routes with a shared branch:

r.on "items", Integer do |item_id|
  # Shared code for branch here

  # /items/123/456
  r.is Integer do |optional_data|
  end

  # /items/123
  r.is do
  end
end

This works well for many cases, but there are also cases where you really want to treat it as one route with an optional segment. One simple way to do that is to use a parameter instead of an optional segment (e.g. /items/123?opt=456).

r.is "items", Integer do |item_id|
  optional_data = r.params['opt'].to_s
end

However, if you really do want to use a optional segment, there are a couple different ways to use matchers to do so. One is using an array matcher where the last element is true:

r.is "items", Integer, [String, true] do |item_id, optional_data|
end

Note that this technically yields only one argument instead of two arguments if the optional segment isn’t provided.

An alternative way to implement this is via a regexp:

r.is "items", /(\d+)(?:\/(\d+))?/ do |item_id, optional_data|
end

Match/Route Block Return Values

If the response body has already been written to by calling response.write directly, then any return value of a match block or route block is ignored.

If the response body has not already been written to, then the match block or route block return value is inspected:

String

used as the response body

nil, false

ignored

everything else

raises an error

Plugins can add support for additional match block and route block return values. One example of this is the json plugin, which allows returning arrays and hashes in match and route blocks and converts those directly to JSON and uses the JSON as the response body.

Status Codes

When it comes time to finalize a response, if a status code has not been set manually and anything has been written to the response, the response will use a 200 status code. Otherwise, it will use a 404 status code. This enables the principle of least surprise to work: if you don’t handle an action, a 404 response is assumed.

You can always set the status code manually, via the status attribute for the response.

route do |r|
  r.get "hello" do
    response.status = 200
  end
end

When redirecting, the response will use a 302 status code by default. You can change this by passing a second argument to r.redirect:

route do |r|
  r.get "hello" do
    r.redirect "/other", 301 # use 301 Moved Permanently
  end
end

Verb Methods

As displayed above, Roda has r.get and r.post methods for matching based on the HTTP request method. If you want to match on other HTTP request methods, use the all_verbs plugin.

When called without any arguments, these match as long as the request has the appropriate method, so:

r.get do end

matches any GET request, and

r.post do end

matches any POST request

If any arguments are given to the method, these match only if the request method matches, all arguments match, and the path has been fully matched by the arguments, so:

r.post "" do end

matches only POST requests where the current path is /.

r.get "a/b" do end

matches only GET requests where the current path is /a/b.

The reason for this difference in behavior is that if you are not providing any arguments, you probably don’t want to also test for an exact match with the current path. If that is something you do want, you can provide true as an argument:

r.on "foo" do
  r.get true do # Matches GET /foo, not GET /foo/.*
  end
end

If you want to match the request method and do only a partial match on the request path, you need to use r.on with the :method hash matcher:

r.on "foo", method: :get do # Matches GET /foo(/.*)?
end

Root Method

As displayed above, you can also use r.root as a match method. This method matches GET requests where the current path is /. r.root is similar to r.get "", except that it does not consume the / from the path.

Unlike the other matching methods, r.root takes no arguments.

Note that r.root does not match if the path is empty; you should use r.get true for that. If you want to match either the empty path or /, you can use r.get ["", true], or use the slash_path_empty plugin.

Note that r.root only matches GET requests. So, to handle POST / requests, use r.post ''.

Request and Response

While the request object is yielded to the route block, it is also available via the request method. Likewise, the response object is available via the response method.

The request object is an instance of a subclass of Rack::Request, with some additional methods.

If you want to extend the request and response objects with additional modules, you can use the module_include plugin.

Pollution

Roda tries very hard to avoid polluting the scope of the route block. This should make it unlikely that Roda will cause namespace issues with your application code. Some of the things Roda does:

  • The only instance variables defined by default in the scope of the route block are @_request and @_response. All instance variables in the scope of the route block used by plugins that ship with Roda are prefixed with an underscore.

  • The main methods defined, beyond the default methods for Object, are env, opts, request, response, and session. call and _call are also defined, but are deprecated. All other methods defined are prefixed with roda

  • Constants inside the Roda namespace are all prefixed with Roda (e.g., Roda::RodaRequest).

Composition

You can mount any Rack app (including another Roda app), with its own middlewares, inside a Roda app, using r.run:

class API < Roda
  route do |r|
    r.is do
      # ...
    end
  end
end

class App < Roda
  route do |r|
    r.on "api" do
      r.run API
    end
  end
end

run App.app

This will take any path starting with /api and send it to API. In this example, API is a Roda app, but it could easily be a Sinatra, Rails, or other Rack app.

When you use r.run, Roda calls the given Rack app (API in this case); whatever the Rack app returns will be returned as the response for the current application.

If you have a lot of rack applications that you want to dispatch to, and which one to dispatch to is based on the request path prefix, look into the multi_run plugin.

hash_branches plugin

If you are just looking to split up the main route block up by branches, you should use the hash_branches plugin, which keeps the current scope of the route block:

class App < Roda
  plugin :hash_branches

  hash_branch "api" do |r|
    r.is do
      # ...
    end
  end

  route do |r|
    r.hash_branches
  end
end

run App.app

This allows you to set instance variables in the main route block and still have access to them inside the api route block.

Testing

It is very easy to test Roda with Rack::Test or Capybara. Roda’s own tests use minitest/spec. The default Rake task will run the specs for Roda.

Settings

Each Roda app can store settings in the opts hash. The settings are inherited by subclasses.

Roda.opts[:layout] = "guest"

class Users < Roda; end
class Admin < Roda
  opts[:layout] = "admin"
end

Users.opts[:layout] # => 'guest'
Admin.opts[:layout] # => 'admin'

Feel free to store whatever you find convenient. Note that when subclassing, Roda only does a shallow clone of the settings.

If you store nested structures and plan to mutate them in subclasses, it is your responsibility to dup the nested structures inside Roda.inherited (making sure to call super). This should be is done so that modifications to the parent class made after subclassing do not affect the subclass, and vice-versa.

The plugins that ship with Roda freeze their settings and only allow modification to their settings by reloading the plugin, and external plugins are encouraged to follow this approach.

The following options are respected by the default library or multiple plugins:

:add_script_name

Prepend the SCRIPT_NAME for the request to paths. This is useful if you mount the app as a path under another app.

:check_arity

Whether arity for blocks passed to Roda should be checked to determine if they can be used directly to define methods or need to be wrapped. By default, for backwards compatibility, this is true, so Roda will check blocks and handle cases where the arity of the block does not match the expected arity. This can be set to :warn to issue warnings whenever Roda detects an arity mismatch. If set to false, Roda does not check the arity of blocks, which can result in failures at runtime if the arity of the block does not match what Roda expects. Note that Roda does not check the arity for lambda blocks, as those are strict by default.

:check_dynamic_arity

Similar to :check_arity, but used for checking blocks where the number of arguments Roda will call the blocks with is not possible to determine when defining the method. By default, Roda checks arity for such methods, but doing so actually slows the method down even if the number of arguments matches the expected number of arguments.

:freeze_middleware

Whether to freeze all middleware when building the rack app.

:json_parser

A callable for parsing JSON (JSON.parse in general used by default).

:json_serializer

A callable for serializing JSON (to_json in general used by default).

:root

Set the root path for the app. This defaults to the current working directory of the process.

:sessions_convert_symbols

This should be set to true if the sessions in use do not support roundtripping of symbols (for example, when sessions are serialized via JSON).

There may be other options supported by individual plugins, if so it will be mentioned in the documentation for the plugin.

Rendering

Roda ships with a render plugin that provides helpers for rendering templates. It uses Tilt, a gem that interfaces with many template engines. The erb engine is used by default.

Note that in order to use this plugin you need to have Tilt installed, along with the templating engines you want to use.

This plugin adds the render and view methods, for rendering templates. By default, view will render the template inside the default layout template; render will just render the template.

class App < Roda
  plugin :render

  route do |r|
    @var = '1'

    r.get "render" do
      # Renders the views/home.erb template, which will have access to
      # the instance variable @var, as well as local variable content.
      render("home", locals: {content: "hello, world"})
    end

    r.get "view" do
      @var2 = '1'

      # Renders the views/home.erb template, which will have access to the
      # instance variables @var and @var2, and takes the output of that and
      # renders it inside views/layout.erb (which should yield where the
      # content should be inserted).
      view("home")
    end
  end
end

You can override the default rendering options by passing a hash to the plugin:

class App < Roda
  plugin :render,
    escape: true, # Automatically escape output in erb templates using Erubi's escaping support
    views: 'admin_views', # Default views directory
    layout_opts: {template: 'admin_layout', engine: 'html.erb'},    # Default layout options
    template_opts: {default_encoding: 'UTF-8'} # Default template options
end

Security

Web application security is a very large topic, but here are some things you can do with Roda to prevent some common web application vulnerabilities.

Session Security

By default, Roda doesn’t turn on sessions, and if you don’t need sessions, you can skip this section. If you do need sessions, Roda offers two recommended ways to implement cookie-based sessions.

If you do not need any session support in middleware, and only need session support in the Roda application, then use the sessions plugin:

require 'roda'
class App < Roda
  plugin :sessions, secret: ENV['SESSION_SECRET']
end

The :secret option should be a randomly generated string of at least 64 bytes.

If you have middleware that need access to sessions, then use the RodaSessionMiddleware that ships with Roda:

require 'roda'
require 'roda/session_middleware'
class App < Roda
  use RodaSessionMiddleware, secret: ENV['SESSION_SECRET']
end

If you need non-cookie based sessions (such as sessions stored in a database), you should use an appropriate external middleware.

It is possible to use other session cookie middleware such as Rack::Session::Cookie, but other middleware may not have the same security features that Roda’s session support does. For example, the session cookies used by the Rack::Session::Cookie middleware provided by Rack before Rack 3 are not encrypted, just signed to prevent tampering.

For any cookie-based sessions, make sure that the necessary secrets (:secret option) are not disclosed to an attacker. Knowledge of the secret(s) can allow an attacker to inject arbitrary session values. In the case of Rack::Session::Cookie, that can also lead remote code execution.

Cross Site Request Forgery (CSRF)

CSRF can be prevented by using the route_csrf plugin that ships with Roda. The route_csrf plugin uses modern security practices to create CSRF tokens, requires request-specific tokens by default, and offers control to the user over where in the routing tree that CSRF tokens are checked. For example, if you are using the public plugin to serve static files and the assets plugin to serve assets, you wouldn’t need to check for CSRF tokens for either of those, so you could put the CSRF check after those in the routing tree, but before handling other requests:

route do |r|
  r.public
  r.assets

  check_csrf! # Must call this to check for valid CSRF tokens

  # ...
end

Cross Site Scripting (XSS)

The easiest way to prevent XSS with Roda is to use a template library that automatically escapes output by default. The :escape option to the render plugin sets the ERB template processor to escape by default, so that in your templates:

<%= '<>' %>  # outputs &lt;&gt; 
<%== '<>' %> # outputs <>

When using the :escape option, you will need to ensure that your layouts are not escaping the output of the content template:

<%== yield %> # not <%= yield %>

This support requires Erubi.

Unexpected Parameter Types

Rack converts submitted parameters into a hash of strings, arrays, and nested hashes. Since the user controls the submission of parameters, you should treat any submission of parameters with caution, and should be explicitly checking and/or converting types before using any submitted parameters. One way to do this is explicitly after accessing them:

# Convert foo_id parameter to an integer
request.params['foo_id'].to_i

However, it is easy to forget to convert the type, and if the user submits foo_id as a hash or array, a NoMethodError will be raised. Worse is if you do:

some_method(request.params['bar'])

Where some_method supports both a string argument and a hash argument, and you expect the parameter will be submitted as a string, and some_method‘s handling of a hash argument performs an unauthorized action.

Roda ships with a typecast_params plugin that can easily handle the typecasting of submitted parameters, and it is recommended that all Roda applications that deal with parameters use it or another tool to explicitly convert submitted parameters to the expected types.

Content Security Policy

The Content-Security-Policy HTTP header can be used to instruct the browser on what types of content to allow and where content can be loaded from. Roda ships with a content_security_policy plugin that allows for the easy configuration of the content security policy. Here’s an example of a fairly restrictive content security policy configuration:

class App < Roda
  plugin :content_security_policy do |csp|
    csp.default_src :none # deny everything by default
    csp.style_src :self
    csp.script_src :self
    csp.connect_src :self
    csp.img_src :self
    csp.font_src :self
    csp.form_action :self
    csp.base_uri :none
    csp.frame_ancestors :none
    csp.block_all_mixed_content
    csp.report_uri 'CSP_REPORT_URI'
  end
end

Other Security Related HTTP Headers

You may want to look into setting the following HTTP headers, which can be done at the web server level, but can also be done at the application level using using the default_headers plugin:

Strict-Transport-Security

Enforces SSL/TLS Connections to the application.

X-Content-Type-Options

Forces some browsers to respect a declared Content-Type header.

X-Frame-Options

Provides click-jacking protection by not allowing usage inside a frame. Only include this if you want to support and protect old browsers that do not support Content-Security-Policy.

Example:

class App < Roda
  plugin :default_headers,
    'Content-Type'=>'text/html',
    'Strict-Transport-Security'=>'max-age=63072000; includeSubDomains',
    'X-Content-Type-Options'=>'nosniff',
    'X-Frame-Options'=>'deny'
end

Rendering Templates Derived From User Input

Roda’s rendering plugin by default checks that rendered templates are inside the views directory. This is because rendering templates outside the views directory is not commonly needed, and it prevents a common attack (which is especially severe if there is any location on the file system that users can write files to).

You can specify which directories are allowed using the :allowed_paths render plugin option. If you really want to turn path checking off, you can do so via the check_paths: false render plugin option.

Code Reloading

Roda does not ship with integrated support for code reloading, but there are rack-based reloaders that will work with Roda apps.

Zeitwerk (which Rails now uses for reloading) can be used with Roda. It requires minimal setup and handles most cases. It overrides require when activated. If it can meet the needs of your application, it’s probably the best approach.

rack-unreloader uses a fast approach to reloading while still being fairly safe, as it only reloads files that have been modified, and unloads constants defined in the files before reloading them. It can handle advanced cases that Zeitwerk does not support, such as classes defined in multiple files (common when using separate route files for different routing branches in the same application). However, rack-unreloader does not modify core classes and using it requires modifying your application code to use rack-unreloader specific APIs, which may not be simple.

AutoReloader provides transparent reloading for all files reached from one of the reloadable_paths option entries, by detecting new top-level constants and removing them when any of the reloadable loaded files changes. It overrides require and require_relative when activated (usually in the development environment). No configurations other than reloadable_paths are required.

rerun uses a fork/exec approach for loading new versions of your app. It work without any changes to application code, but may be slower as they have to reload the entire application on every change. However, for small apps that load quickly, it may be a good approach.

There is no one reloading solution that is the best for all applications and development approaches. Consider your needs and the tradeoffs of each of the reloading approaches, and pick the one you think will work best. If you are unsure where to start, it may be best to start with Zeitwerk, and only consider other options if it does not work well for you.

Plugins

By design, Roda has a very small core, providing only the essentials. All nonessential features are added via plugins.

Roda’s plugins can override any Roda method and call super to get the default behavior, which makes Roda very extensible.

Roda ships with a large number of plugins, and some other libraries ship with support for Roda.

How to create plugins

Authoring your own plugins is pretty straightforward. Plugins are just modules, which may contain any of the following modules:

InstanceMethods

module included in the Roda class

ClassMethods

module that extends the Roda class

RequestMethods

module included in the class of the request

RequestClassMethods

module extending the class of the request

ResponseMethods

module included in the class of the response

ResponseClassMethods

module extending the class of the response

If the plugin responds to load_dependencies, it will be called first, and should be used if the plugin depends on another plugin.

If the plugin responds to configure, it will be called last, and should be used to configure the plugin.

Both load_dependencies and configure are called with the additional arguments and block that was given to the plugin call.

So, a simple plugin to add an instance method would be:

module MarkdownHelper
  module InstanceMethods
    def markdown(str)
      BlueCloth.new(str).to_html
    end
  end
end

Roda.plugin MarkdownHelper

Registering plugins

If you want to ship a Roda plugin in a gem, but still have Roda load it automatically via Roda.plugin :plugin_name, you should place it where it can be required via roda/plugins/plugin_name and then have the file register it as a plugin via Roda::RodaPlugins.register_plugin. It’s recommended, but not required, that you store your plugin module in the Roda::RodaPlugins namespace:

class Roda
  module RodaPlugins
    module Markdown
      module InstanceMethods
        def markdown(str)
          BlueCloth.new(str).to_html
        end
      end
    end

    register_plugin :markdown, Markdown
  end
end

To avoid namespace pollution, you should avoid creating your module directly in the Roda namespace. Additionally, any instance variables created inside InstanceMethods should be prefixed with an underscore (e.g., @_variable) to avoid polluting the scope. Finally, do not add any constants inside the InstanceMethods module, add constants to the plugin module itself (Markdown in the above example).

If you are planning on shipping your plugin in an external gem, it is recommended that you follow standard gem naming conventions for extensions. So if your plugin module is named FooBar, your gem name should be roda-foo_bar.

No Introspection

Because a routing tree does not store the routes in a data structure, but directly executes the routing tree block, you cannot introspect the routes when using a routing tree.

If you would like to introspect your routes when using Roda, there is an external plugin named roda-route_list, which allows you to add appropriate comments to your routing files, and has a parser that will parse those comments into routing metadata that you can then introspect.

Inspiration

Roda was inspired by Sinatra and Cuba. It started out as a fork of Cuba, from which it borrows the idea of using a routing tree (which Cuba in turn took from Rum). From Sinatra, it takes the ideas that route blocks should return the request bodies and that routes should be canonical. Roda’s plugin system is based on the plugin system used by Sequel.

Ruby Support Policy

Roda fully supports the currently supported versions of Ruby (MRI) and JRuby. It may support unsupported versions of Ruby or JRuby, but such support may be dropped in any minor version if keeping it becomes a support issue. The minimum Ruby version required to run the current version of Roda is 1.9.2, and the minimum JRuby version is 9.0.0.0.

License

MIT

Maintainer

Jeremy Evans <[email protected]>

roda's People

Contributors

adam12 avatar aekobear avatar celsworth avatar chrisfrank avatar cyx avatar docelic avatar eregon avatar evanleck avatar fnordfish avatar freaky avatar frodsan avatar hmdne avatar janko avatar jeremyevans avatar kematzy avatar kenaniah avatar luciusgone avatar martinpoljak avatar mindreframer avatar papierkorb avatar plukevdh avatar pusewicz avatar richmorin avatar rkh avatar rosenfeld avatar rpag avatar skade avatar soveran avatar vais avatar vangberg avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

roda's Issues

Plugin :assets, a path to an asset file(s)

  1. How can I get a path to a single asset such as a image from a view? Something like:

<img src="<%= asset_path("my_image.png") %>" />

  1. Do I have to enum all images I use accross my application in the "rb" file to be able to access them from views? Something like:
 # config.ru
  plugin :assets, 
    images: {img1: "my_image1.png", img2: "my_image2.png"}

   # view.erb
   <img src="<%= asset_path(:img1) %>" />

If so, is there a way to avoid that and merely use an image in a view without having to declare it the way I've shown above?

render plugin fails under slim

The following code works:

require 'roda'
require 'slim'

class SlimTest < Roda
  plugin :render, engine: 'slim'

  route do |r|
    r.root do 
      render :index
    end
  end
end

run SlimTest.freeze.app

If you change render to view (with a layout.slim in the right spot), the app stops responding completely. I get "No data received in Chrome". Change it to haml or erb and it works great. Not sure what's happening, but I'm pretty stuck. Ideas?

Alternatives for Block Spaghetti

How do you avoid it?

Looking at an example from the README:

r.on "a" do           # /a branch
  check_perm(:A)  
  r.on "b" do         # /a/b branch
    check_perm(:B)  
    r.is "c" do       # /a/b/c request
      r.get do end    # GET  /a/b/c request
      r.post do end   # POST /a/b/c request
    end
    r.get "d" do end  # GET  /a/b/d request
    r.post "e" do end # POST /a/b/e request
  end
end

gives me the impression that Roda lends itself to the same pitfalls that Sinatra does, but even more so. The examples also have no implementation. Once you go implement code, you could already be 3 or 4 block-levels deep.

Is there any way, or plans, to map a routing tree to a class that doesn't use blocks as entry point, and less like a DSL? I know this is likely a mailing list question, sorry.

[Assets Plugin] Accept string options

This is an example of how the assets plugin is loaded in my application, it's a fairly small project yet:

# site
class Site < Roda
  #
  plugin :assets, group_subdirs: false,
         css: { site: %w[application.css.sass] },
         js:  { site: %w[lib/jquery-2.1.3.js lib/chosen.jquery.js lib/jquery.mask.js bootstrap/bootstrap.min.js lib/bootstrap-slider.js home.js rent_registration.js] },
         dependencies: { 'assets/css/application.css.sass' => ['assets/css/**/*.sass' 'assets/css/**/*.scss'] }
  #
end

Notice how this is not scalable and pollutes the main file for the Roda application. To fix this I implemented the following structure:

class Site < Roda
  #
  plugin :assets, YAML.load_file('assets/manifest.yml')
  #
end
---
# assets/manifest.yml

css:
  site:
    - application.css.sass

js:
  site: 
    - lib/jquery-2.1.3.js
    - lib/chosen.jquery.js
    - lib/jquery.mask.js
    - lib/bootstrap.js
    - lib/bootstrap-slider.js
    - home.js
    - rent_registration.js

dependencies:
  assets/css/application.css.sass:
    - assets/css/**/*.sass
    - assets/css/**/*.scss

group_subdirs: false

This is much more elegant and organized.

The only problem is that the assets plugin expects symbols as arguments whereas the keys of YAML dictionaries are loaded as strings.

I am filing up this issue to ask whether it is feasible to implement this or not considering the plugin structure and if it's desirable. If so, I can send the PR myself.

I am using the Hash#deep_symbolize_keys extension that comes with I18n to workaround this.

Can't render rabl templates due to frozen opts passed as locals

The Rabl engine for Tilt changes the locals hash state when rendering (see https://github.com/nesquena/rabl/blob/master/lib/rabl/engine.rb#L405).

The render plugin however provides the OPTS hash as locals when none is provided, but this hash is frozen, which gives a RuntimeError when it gets to the the aforementioned line. (See https://github.com/jeremyevans/roda/blob/master/lib/roda/plugins/render.rb#L149 and

retrieve_template(opts).render((opts[:scope]||self), (opts[:locals]||OPTS), &block)
).

The workaround is to explicitly pass locals: {} when calling render.

The cause of this error is somewhat evident, but if necessary, I can provide an app with the minimal reproduction.

Backtrace:

``` RuntimeError - can't modify frozen Hash: rabl (0.13.1) lib/rabl/engine.rb:405:in `set_locals' rabl (0.13.1) lib/rabl/engine.rb:401:in `set_instance_variables!' rabl (0.13.1) lib/rabl/engine.rb:30:in `apply' rabl (0.13.1) lib/rabl/template.rb:15:in `evaluate' tilt (2.0.7) lib/tilt/template.rb:109:in `render' roda (2.25.0) lib/roda/plugins/render.rb:265:in `render' lib/mastodon/api.rb:31:in `block (3 levels) in ' roda (2.25.0) lib/roda.rb:767:in `always' roda (2.25.0) lib/roda.rb:758:in `_verb' roda (2.25.0) lib/roda.rb:388:in `get' lib/mastodon/api.rb:20:in `block (2 levels) in ' roda (2.25.0) lib/roda.rb:832:in `if_match' roda (2.25.0) lib/roda.rb:464:in `is' lib/mastodon/api.rb:19:in `block in ' roda (2.25.0) lib/roda.rb:261:in `block in call' roda (2.25.0) lib/roda.rb:259:in `call' roda (2.25.0) lib/roda.rb:228:in `block in build_rack_app' warden (1.2.7) lib/warden/manager.rb:32:in `call' roda (2.25.0) lib/roda.rb:117:in `call' actionpack (5.0.2) lib/action_dispatch/routing/mapper.rb:17:in `block in ' actionpack (5.0.2) lib/action_dispatch/routing/mapper.rb:46:in `serve' actionpack (5.0.2) lib/action_dispatch/journey/router.rb:39:in `block in serve' actionpack (5.0.2) lib/action_dispatch/journey/router.rb:26:in `serve' actionpack (5.0.2) lib/action_dispatch/routing/route_set.rb:725:in `call' bullet (5.5.1) lib/bullet/rack.rb:12:in `call' http_accept_language (2.1.0) lib/http_accept_language/middleware.rb:14:in `call' rack (2.0.1) lib/rack/deflater.rb:35:in `call' rack-attack (5.0.1) lib/rack/attack.rb:147:in `call' warden (1.2.7) lib/warden/manager.rb:36:in `block in call' warden (1.2.7) lib/warden/manager.rb:35:in `call' rack (2.0.1) lib/rack/etag.rb:25:in `call' rack (2.0.1) lib/rack/conditional_get.rb:25:in `call' rack (2.0.1) lib/rack/head.rb:12:in `call' rack (2.0.1) lib/rack/session/abstract/id.rb:222:in `context' rack (2.0.1) lib/rack/session/abstract/id.rb:216:in `call' actionpack (5.0.2) lib/action_dispatch/middleware/cookies.rb:613:in `call' activerecord (5.0.2) lib/active_record/migration.rb:553:in `call' actionpack (5.0.2) lib/action_dispatch/middleware/callbacks.rb:38:in `block in call' activesupport (5.0.2) lib/active_support/callbacks.rb:97:in `__run_callbacks__' activesupport (5.0.2) lib/active_support/callbacks.rb:750:in `_run_call_callbacks' activesupport (5.0.2) lib/active_support/callbacks.rb:90:in `run_callbacks' actionpack (5.0.2) lib/action_dispatch/middleware/callbacks.rb:36:in `call' actionpack (5.0.2) lib/action_dispatch/middleware/executor.rb:12:in `call' actionpack (5.0.2) lib/action_dispatch/middleware/remote_ip.rb:79:in `call' better_errors (2.1.1) lib/better_errors/middleware.rb:84:in `protected_app_call' better_errors (2.1.1) lib/better_errors/middleware.rb:79:in `better_errors_call' better_errors (2.1.1) lib/better_errors/middleware.rb:57:in `call' actionpack (5.0.2) lib/action_dispatch/middleware/debug_exceptions.rb:49:in `call' actionpack (5.0.2) lib/action_dispatch/middleware/show_exceptions.rb:31:in `call' railties (5.0.2) lib/rails/rack/logger.rb:36:in `call_app' railties (5.0.2) lib/rails/rack/logger.rb:24:in `block in call' activesupport (5.0.2) lib/active_support/tagged_logging.rb:69:in `block in tagged' activesupport (5.0.2) lib/active_support/tagged_logging.rb:26:in `tagged' activesupport (5.0.2) lib/active_support/tagged_logging.rb:69:in `tagged' railties (5.0.2) lib/rails/rack/logger.rb:24:in `call' sprockets-rails (3.2.0) lib/sprockets/rails/quiet_assets.rb:13:in `call' actionpack (5.0.2) lib/action_dispatch/middleware/request_id.rb:24:in `call' rack (2.0.1) lib/rack/method_override.rb:22:in `call' rack (2.0.1) lib/rack/runtime.rb:22:in `call' rack-timeout (0.4.2) lib/rack/timeout/core.rb:100:in `call' activesupport (5.0.2) lib/active_support/cache/strategy/local_cache_middleware.rb:28:in `call' actionpack (5.0.2) lib/action_dispatch/middleware/executor.rb:12:in `call' actionpack (5.0.2) lib/action_dispatch/middleware/static.rb:136:in `call' rack (2.0.1) lib/rack/sendfile.rb:111:in `call' rack-cors (0.4.1) lib/rack/cors.rb:81:in `call' railties (5.0.2) lib/rails/engine.rb:522:in `call' puma (3.8.2) lib/puma/configuration.rb:224:in `call' puma (3.8.2) lib/puma/server.rb:600:in `handle_request' puma (3.8.2) lib/puma/server.rb:435:in `process_client' puma (3.8.2) lib/puma/server.rb:299:in `block in run' puma (3.8.2) lib/puma/thread_pool.rb:120:in `block in spawn_thread' ```

NoMethodError when using content_for

require 'roda'
class App < Roda
  plugin :content_for
  plugin :render
  route do |r|
    r.get do
      render(inline: '<% content_for :foo do %><% end %>')
    end
  end
end
run App.freeze.app
# NoMethodError: undefined method `scan' for nil:NilClass
# 	/var/lib/gems/2.3.0/gems/erubi-1.7.0/lib/erubi.rb:94:in `initialize'
# 	/var/lib/gems/2.3.0/gems/tilt-2.0.7/lib/tilt/erubi.rb:19:in `new'
# 	/var/lib/gems/2.3.0/gems/tilt-2.0.7/lib/tilt/erubi.rb:19:in `prepare'
# 	/var/lib/gems/2.3.0/gems/tilt-2.0.7/lib/tilt/template.rb:99:in `initialize'
#       /var/lib/gems/2.3.0/gems/roda-3.4.0/lib/roda/plugins/content_for.rb:65:in `new'
#       /var/lib/gems/2.3.0/gems/roda-3.4.0/lib/roda/plugins/content_for.rb:65:in `content_for'
#       <% content_for :foo do %><% end %>:1:in `block in singleton class'

Slow regexp matching

Hi, I have noticed an unexpected behaviour of a custom regexp used in Roda. Sometimes it was very slow (the slower, the longer the url was). Steps to reproduce with Roda 3.1.0 (and perhaps also older versions):

url = "/foo/bar/baz-bar-bar-bar-bar-bar-bar.html"

# pattern used in r.on to match one or multiple directories from the url
pattern = /(([\w\d_-]+\/?)+)/

# this is how Roda includes the pattern in it's consume_pattern
url =~ /\A\/(?:#{pattern})(?=\/|\z)/

The problem is the ending bracket (?=/|\z). I have tried several variants with these results:

(?=\/)  # slow
(\z)     # slow
(\/?)    # fast

So I have used the last variant with no \z and no look-ahead. Therefore I had to re-add the possibly matched slash. Roda's code changes:

   def consume_pattern(pattern)
     /\A\/(?:#{pattern})(\/?)/
   end

   def _match_class_Integer
     consume(/\A\/(\d+)(\/?)/){|i| [i.to_i]}
   end

   def consume(pattern)
     if matchdata = remaining_path.match(pattern)
       @remaining_path = matchdata.captures[-1] + matchdata.post_match
       captures = matchdata.captures[0..-2]
       captures = yield(*captures) if block_given?
       @captures.concat(captures)
     end
   end

It works for me, but still I don't know the purpose of the "\z" in the wrapping pattern? Is it necessary to be there?

Incorrect behaviour for params.has_key?(:key) with indifferent_params plugin

Let's say I have a roda router with an indifferent_params plugin.

The error is described by the following code:

# GET /users?user_id=1
r.get do
  params[:user_id]
  #=> 1, as expected

  params.has_key?('user_id')
  #=> true, as expected

  params.has_key?(:user_id)
  #=> false, but expected result is true
  # as well as #fetch(:user_id) and possibly some other hash methods
end

type_routing plugin does not appear to work for sub apps when loaded on parent app

Given the following app:

class App < Roda
  plugin :type_routing, use_headers: false
end

class Sub < App
  route do |r|
    r.get 'a' do
      r.html{ "<h1>This is the HTML response</h1>" }
      r.json{ '{"json": "ok"}' }
      r.xml{ "<root>This is the XML response</root>" }
      "Unsupported data type"
    end
  end
end

class Routes < App
  route do |r|
    r.on "subpath", proc { r.run Sub }
  end
end

class App
  route do |r|
    r.run Routes
  end
end

The app does not route by type as expected:

  • /subpath/a -> html (works)
  • /subpath/a.html -> html (works)
  • /subpath/a.json -> html (broken)
  • /subpath/a.xml -> html (broken)

halt plugin: 204 not properly handled

I'm using roda with halt plugin. For the delete route case I wanted to reply with 204 status code, so I have:

  @resource.destroy
  r.halt 204

rack breaks with Rack::Lint::LintError: Content-Type header found in 204 response, not allowed. This seems to be due to roda, which by default sets the response default headers and the Content-Type. Roda should detect if the status of the response is already set, and if so, only set Content-Type when it's allowed. List of status codes is here. This is where roda is setting them.

Does Roda Caching Plugin work with SSL?

Hello,
I am thinking of switching my site to https. But I just wanted to make sure that Roda cache supports SSL because I am making heavy use of the plugin. I am using nginx and thin. There's no login system on the site, its more like a blog but I just wanted to switch it to SSL for its other benefits.

thank you.

Docs might be not up to date

Hi! I had a little confusion on including my assets into my Roda app. I was following the doc and did <%= assets(:js) %> and my browser render literal <script /> tag into the view. It turns out I need to do double equal sign == which will be <%== assets(:js) %>instead of single equal sign while the docs here http://roda.jeremyevans.net/rdoc/classes/Roda/RodaPlugins/Assets.html#module-Roda::RodaPlugins::Assets-label-Views didn't mention so or I probably missed it somewhere.

Unable to test releases

Am I missing a step for running the specs? After unpacking the roda gem, when I run rake spec I get this (first 3 of 14 shown):

  1) Error:
static plugin#test_0001_adds support for serving static files:
Errno::ENOENT: No such file or directory @ rb_sysopen - spec/views/about/_test.erb
    /wrkdirs/usr/ports/www/rubygem-roda/work/roda-3.4.0/spec/plugin/static_spec.rb:14:in `read'
    /wrkdirs/usr/ports/www/rubygem-roda/work/roda-3.4.0/spec/plugin/static_spec.rb:14:in `block (2 levels) in <top (required)>'

  2) Error:
static plugin#test_0002_respects the application's :root option:
Errno::ENOENT: No such file or directory @ rb_sysopen - spec/views/about/_test.erb
    /wrkdirs/usr/ports/www/rubygem-roda/work/roda-3.4.0/spec/plugin/static_spec.rb:28:in `read'
    /wrkdirs/usr/ports/www/rubygem-roda/work/roda-3.4.0/spec/plugin/static_spec.rb:28:in `block (2 levels) in <top (required)>'

  3) Error:
timestamp_public plugin#test_0001_adds r.timestamp_public for serving static files from timestamp_public folder:
Errno::ENOENT: No such file or directory @ rb_sysopen - spec/views/about/_test.erb
    /wrkdirs/usr/ports/www/rubygem-roda/work/roda-3.4.0/spec/plugin/timestamp_public_spec.rb:17:in `read'
    /wrkdirs/usr/ports/www/rubygem-roda/work/roda-3.4.0/spec/plugin/timestamp_public_spec.rb:17:in `block (2 levels) in <top (required)>'

json-parser plugin does not handle DELETE requests

I'm using Roda to implement a JSONAPI API. I'm attempting to use the json-parser plugin to parse incoming JSON bodies. Unfortunately, this only seems to work for POST and PATCH requests -- it doesn't work for DELETE (and it needs to for JSONAPI). The documentation does not mention that request bodies are parsed only for certain HTTP methods. And when I look at the source, I can't see any way to change this behavior. It seems like the conditions under which the parser is invoked should at least be documented and, even better, configurable.

Some route issue. (About put, delete and etc.)

Hello, guys!

So, i tried to understand how it works. And now i have a question.

Why we have get and post methods and no put, delete and other?

Maybe there is a way? If i wanna write some restful api with beautiful tree routing - i have to come up with some bad routes...

How can i fix it?

Websockets

I'm experimenting with a websockets app using Roda on Puma, using the example Roda Websockets chat app:

TypeError: no implicit conversion of Rack::Lint::HijackWrapper into Integer
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/eventmachine-1.0.8/lib/eventmachine.rb:762:in `attach_fd'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/eventmachine-1.0.8/lib/eventmachine.rb:762:in `attach_io'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/eventmachine-1.0.8/lib/eventmachine.rb:745:in `attach'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/faye-websocket-0.10.0/lib/faye/rack_stream.rb:26:in `initialize'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/faye-websocket-0.10.0/lib/faye/websocket.rb:70:in `new'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/faye-websocket-0.10.0/lib/faye/websocket.rb:70:in `initialize'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/roda-2.5.1/lib/roda/plugins/websockets.rb:92:in `new'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/roda-2.5.1/lib/roda/plugins/websockets.rb:92:in `block in websocket'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/roda-2.5.1/lib/roda.rb:713:in `always'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/roda-2.5.1/lib/roda/plugins/websockets.rb:90:in `websocket'
    /Users/jamie/Code/clearwater-chat/clearwater-chat.rb:17:in `block (2 levels) in <class:ClearwaterChat>'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/roda-2.5.1/lib/roda.rb:771:in `if_match'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/roda-2.5.1/lib/roda.rb:494:in `on'
    /Users/jamie/Code/clearwater-chat/clearwater-chat.rb:15:in `block in <class:ClearwaterChat>'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/roda-2.5.1/lib/roda.rb:250:in `instance_exec'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/roda-2.5.1/lib/roda.rb:250:in `block in call'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/roda-2.5.1/lib/roda.rb:248:in `catch'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/roda-2.5.1/lib/roda.rb:248:in `call'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/roda-2.5.1/lib/roda.rb:222:in `block in build_rack_app'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/roda-2.5.1/lib/roda.rb:114:in `call'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/roda-2.5.1/lib/roda.rb:114:in `call'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/rack-1.6.4/lib/rack/tempfile_reaper.rb:15:in `call'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/rack-1.6.4/lib/rack/lint.rb:49:in `_call'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/rack-1.6.4/lib/rack/lint.rb:37:in `call'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/rack-1.6.4/lib/rack/showexceptions.rb:24:in `call'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/rack-1.6.4/lib/rack/commonlogger.rb:33:in `call'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/rack-1.6.4/lib/rack/chunked.rb:54:in `call'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/rack-1.6.4/lib/rack/content_length.rb:15:in `call'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/puma-2.13.4/lib/puma/server.rb:541:in `handle_request'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/puma-2.13.4/lib/puma/server.rb:388:in `process_client'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/puma-2.13.4/lib/puma/server.rb:270:in `block in run'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/puma-2.13.4/lib/puma/thread_pool.rb:106:in `call'
    /Users/jamie/.rvm/gems/ruby-2.2.3@clearwater-roda/gems/puma-2.13.4/lib/puma/thread_pool.rb:106:in `block in spawn_thread'
::1 - - [06/Sep/2015:13:24:18 -0400] "GET /chat HTTP/1.1" 500 3570 0.0019

Here's what I'm running on:

  • Ruby 2.2.3
  • Roda 2.5.1
  • Puma 2.13.4
  • Faye::Websocket 0.10.0

Plugins SymbolStatus and Halt don't work well together

When using both plugins :halt and :symbol_status combined, calling halt raises and error when passing the response symbol but works fine with the response code number:

class App < Roda
  plugin :halt
  plugin :symbol_status

  route do |r|

    post 'thingy' do
      Thingy.create(r.params)
      halt 201 # Works fine
    end

    post 'entity' do
      Entity.create(r.params)
      halt :no_content # Raises an error
    end

  end

end

Add content-type to plugin: json

Akward, read the guidlines after I pressed submit.
Sorry.

I'm having problems adding content-type and using the json-plugin. My output contains Cyrillic-letter and so I need to set the content-type to be UTF-8 so that the browsers renders the text correctly (at least that's my understanding of how it works).

So when I try to add a default-header or change it before my JSON is returned, it get overwritten by the json-plugin.
See https://github.com/jeremyevans/roda/blob/master/lib/roda/plugins/json.rb#L84

Am I working on the problem from the wrong angle, or should maybe the plugin be extended so that you can supplement the content-type with charset?

Assets plugin can match any route

I noticed a minute ago that my app was returning a JSON structure for an URL that I expected to get a 404 back from. Here's the code:

require 'roda'
require 'sass'

class App < Roda
    plugin :assets, {
        css: {
            app: %w(reset.scss),
        },  
    }   
    plugin :json

    route do |r|
        r.assets
    end 

end

run App

I would expect this code to return 404 for anything but /assets/css/app/reset.scss? But no:

ubuntu:~> GET -Se http://localhost:9292/
GET http://localhost:9292/
200 OK 
Connection: close
Date: Tue, 12 May 2015 22:27:59 GMT
Server: WEBrick/1.3.1 (Ruby/2.2.1/2015-02-26)
Content-Length: 63
Content-Type: application/json
Client-Date: Tue, 12 May 2015 22:27:59 GMT
Client-Peer: 127.0.0.1:9292
Client-Response-Num: 1

[["css","(?-mix:assets\\/css\\/((?-mix:app\\/reset\\.scss)))"]] 

It seems the structure returned by r.assets is being interpreted by the json plugin and matching any route? The previous advice I've had about r.assets is to place it at the end, but this might present problems? It's certainly not what I expected to happen :)

Error using content_for on test environment

After upgrading Roda to 2.8.0 I've been receiving the following error on my test suite, this happens when calling content_for with a block, my engine is Slim:

NoMethodError: undefined method `encoding' for nil:NilClass
    /home/badosu/.gem/ruby/2.2.3/gems/temple-0.7.6/lib/temple/filters/remove_bom.rb:8:in `call'
    /home/badosu/.gem/ruby/2.2.3/gems/temple-0.7.6/lib/temple/engine.rb:50:in `block in call'
    /home/badosu/.gem/ruby/2.2.3/gems/temple-0.7.6/lib/temple/engine.rb:50:in `each'
    /home/badosu/.gem/ruby/2.2.3/gems/temple-0.7.6/lib/temple/engine.rb:50:in `inject'
    /home/badosu/.gem/ruby/2.2.3/gems/temple-0.7.6/lib/temple/engine.rb:50:in `call'
    /home/badosu/.gem/ruby/2.2.3/gems/temple-0.7.6/lib/temple/mixins/template.rb:10:in `compile'
    /home/badosu/.gem/ruby/2.2.3/gems/temple-0.7.6/lib/temple/templates/tilt.rb:30:in `prepare'
    /home/badosu/.gem/ruby/2.2.3/gems/tilt-2.0.1/lib/tilt/template.rb:87:in `initialize'
    /home/badosu/.gem/ruby/2.2.3/gems/roda-2.8.0/lib/roda/plugins/content_for.rb:41:in `new'
    /home/badosu/.gem/ruby/2.2.3/gems/roda-2.8.0/lib/roda/plugins/content_for.rb:41:in `content_for'

Any idea what could it be?

Roda::Request#halt returns nil headers

In Puma, r.halt(404) returns a nil headers hash. In puma 3.9.1 /lib/puma/server.rb:679, puma calls headers.each, resulting in a server failure and a 500. I can't tell if this is an error on the part of Puma assuming headers will be non-nil or of Roda for returning nil headers.

compile_assets does not work across multiple servers

When using compile_assets on say heroku, the unique id is different for every dyno and the dynos won't always point back to the same dyno which ends up in a 404. I think to solve this we need an option to set our own unique key.

Improve user experience when using Puma

Hi Jeremy, I'm not sure whether this Puma's behavior is intentional or not. But try running this on Puma:

# config.ru
require 'roda'

class App < Roda
  route do |r|
    require_relative 'abc' # it seems like Exceptions inheriting RuntimeError do not cause this, so maybe this is a bug. I'll report to Puma too.
  end
end

run App

# abc.rb
class # any invalid code

In Webrick we can see immediately what is the problem but Puma will ignore it silently and would cause ERR_EMPTY_RESPONSE error in Chrome. I guess it would be a good idea for Puma to rescue Exception on call and take some action rather than letting it propagate to the web server. What do you think?

Rack 2.0?

Hi

I see that rack has a 2.0.1 release. Does Roda works with rack 2.x?

Thanks

How to implement something like Grape's inline API documentation

I'm a big fan of the Grape documentation that goes like:

    desc 'OAuth endpoint'
    params do
      requires :provider_name, type: String, values: ['google', 'asana'], desc: 'The provider, such as google or asana'
      requires :state, type: String, desc: 'A securely random string'
      requires :redirect_uri, type: String, desc: 'Final destination to redirect to (on the client)'
      optional :access_token, type: String, desc: 'The access token that you will be using'
    end
    get 'oauth/:provider_name' do
       # ...
    end

Any idea how I could accomplish something similar in Roda? Thanks!

"foobar" in Matchers section of README -- where'd it go?

https://github.com/jeremyevans/roda/blob/master/README.rdoc#matchers has:

   # GET /username/foobar branch
    r.on "username", String, method: :get do |username|
      user = User.find_by_username(username)

      # GET /username/foobar/posts
      r.is "posts" do
        # You can access user here, because the blocks are closures.
        "Total Posts: #{user.posts.size}" #=> "Total Posts: 6"
      end

Is the foobar actually getting eaten by the matcher, or is this a documentation error?

routing issue

Using the example code below:

require "roda"

class Test < Roda
  route do |r|
    r.is "" do
      "Test"
    end
  end
end

class App < Roda
  route do |r|
    r.is "" do
      'Hello, World!'
    end

    r.on 'test' do
      r.run Test
    end
  end

  plugin :not_found do
    '404'
  end
end

visiting http://localhost shows Hello, World! same with http://localhost/. but when you visit http://localhost/test you get a 404, only when you visit http://localhost/test/ will it work and display Test.

p.s. thank you for this great framework.

Live / Streaming API?

Not an issue as much as an inquiry. I am THIS close to selling my team on Roda, but we do need a bit of async action, mostly in the form of SSE, though there could be room for a full on pub/sub solution down the line.

Curious if you have ever dealt with these specs or how plausible it even is within the scope of a plugin. My own research shows people reaching a dead end in terms of mixing synchronous with asynchronous code, like the ill-fated Rack Stream library.

I am personally considering Rack AMQP to fill this void, but the jury is still out until I can really kick the tires on this badboy.

Thoughts?

Make hooks work with static routes

before/after hooks (enabled by the hooks plugin) do not work with static routes (enabled by the static_routing plugin). Is there a way to make them work together or are they completely incompatible with each other?

Documentation update: Roda looks 4x faster than Sinatra

Hi! according to recent results based on Hello World bench from https://github.com/luislavena/bench-micro#requestssec

On Ruby 2.5 and latest versions of Sinatra (2.0.1) and Roda (3.5.0) I have avg results close to this:

Framework            Requests/sec  % from best
----------------------------------------------
rack                     15131.92      100.00%
watts                    14194.38       93.80%
syro                     13474.00       89.04%
roda                     13093.14       86.53%
hobbit                   12498.27       82.60%
hanami-router            12016.36       79.41%
cuba                     11940.37       78.91%
rack-response            11663.71       77.08%
hobby                    11490.01       75.93%
newark                   10064.76       66.51%
plezi                     9603.98       63.47%
rambutan                  9560.19       63.18%
rack-app                  8226.16       54.36%
rackstep                  7181.35       47.46%
rails-metal               5725.60       37.84%
flame                     5053.43       33.40%
sinatra                   3234.30       21.37%
grape                     3154.16       20.84%
rails-api                 1326.70        8.77%

Which hints that you should update docs and fix Roda 3x advantage over Sinatra to 4x

I believe it may be make sense to mention ~10x advantage over Rails API too

`content_for` with non-erb templating engines

Playing with HAML in particular and content_for doesn't work (generally fails to render or renders inline rather than in specified content_for :my_tag location).

I'm unsure why it was implemented with knowledge of @_out_buf rather than: a) something more template engine agnostic using some Tilt mechanism or b) a more straightforward bit of code like so:

def content_for(key, &block)
  if block
    @content_for ||= {}
    @content_for[key] = yield
  else
    @content_for && @content_for[key]
  end
end

I am willing to work on a PR for this if there is no existing reasoning for the current code that I am unaware of.

Thanks!

JSON body POST

I'm sorry I must be doing something wrong but not getting it.

I am POSTing using JS

fetch('/movies', {
  method: 'post',
  headers: {
    'Accept': 'application/json'
  },
  body: JSON.stringify({
    name: 'Hubot',
    login: 'hubot',
  })
})

to my Roda endpoint

plugin :json
plugin :json_parsed

r.post 'movies' do
  r.params #=> {}
  JSON.parse request.body.as_json["input"][0] #=> {name: 'Hub ...
end
to my Roda endpoint 

UPDATE: (less stupid)

JSON.parse r.body.read => {name: 'Hubot'....

why am I not getting r.params or params?
I know the data is there as part of the body since it works the long way (ie wrong and hard way).
Thanks for any help. I'm sure I'm doing something wrong since others have gotten this to work.

Is it possible to redirect an error page in Roda?

I was wondering if it is possible to redirect a 404 or any other error page in roda to some other page lets say the homepage. I am using both error_handler and status_handler plugins but from the docs it appears they need to be used outside the route do |r| end. Because of that, I am not able to redirect it. So is there a way to do that?

thanks

Idiomatic way to implement an authenticate method

Suppose I have something like this:

class Tasks < Roda
  route do |r|
    r.on 'tasks' do
      authenticate!

      r.get do
        # ...
      end

      r.get ':id' do
        # ...
      end
    end
  end

end

The behavior I want is to return a 401 status code with a json error message if authorization header isn't being passed in properly. I don't want the following r.get statements to get called if the user isn't authenticated.

What's the idiomatic way to deal with this given that have access to the current_user?

Thanks!

public plugin

Hello!

It seems the public static file service plugin is not currently functional.

example:

# config.ru
require 'roda'
class App < Roda
  plugin :public
  route { |r| r.root { "test" } }
end

run App.freeze.app

where ./public/index.html exists

GET /index.html => 404

Named Routes

Probably one of the biggest things I miss from Rails router is named routes. A lot of times I will build a modular app which gets namespaced under other apps and I need the links to be dynamic.

I personally love how Lotus does it: https://github.com/lotus/router#named-routes

Curious to see how this would work with Roda's tree style routes. Surely there must be a way?

Locals aren't rendered in the layout (render plugin)

Given the following app:

# app.rb
require 'roda'

class App < Roda
  plugin :render

  route do |r|
    r.root do
      @instance = 'Instance Var'
      view 'test', locals: { local: 'Local Var' }
    end
  end
end
#views/layout.erb
<html><body>
<h2>Layout</h2>
<p>
Instance: <%= defined?(@instance) ? @instance : 'None'  %><br>
Local: <%= defined?(local) ? local : 'None' %>
</p>

<%= yield %>
</body></html>
#views/test.erb
<h2>View</h2>

<p>
Instance: <%= defined?(@instance) ? @instance : 'None'  %><br>
Local: <%= defined?(local) ? local : 'None' %>
</p>

The outcome is:

Layout

Instance: Instance Var
Local: None

View

Instance: Instance Var
Local: Local Var

Instead of

Layout

Instance: Instance Var
Local: Local Var

View

Instance: Instance Var
Local: Local Var

How to redirect to external page?

I'm sorry if there is already a solution. I try to find it but no luck.

Currently, in my Roda app, I would like to redirect user to an external page (says www.foo.bar).
However, looks like Roda app will redirect to /www.foo.bar.

Here is the code:

r.root do
    r.redirect "www.foo.bar" # This will be redirect to '/www.foo.bar'
end

Is there any way to achieve this?
Thank you

match_affix plugin doesn't work properly

I am trying to use the match_affix plugin so that it consumes the trailing slash. When match_affix is used with r.on, it doesn't work. However, it works when the full route is used with r.get. eg, when i open localhost:9292/hello/world/ the first code gives a blank page, while the second one works fine.

require "roda"
class App < Roda
plugin :match_affix, nil, /(\/|\z)/

  route do |r|
    r.root do
      r.redirect "/hello"
    end

    r.on "hello" do
      r.get "world" do
        "hello world!"
      end
end
end
end

Code 2:

require "roda"
class App < Roda
plugin :match_affix, nil, /(\/|\z)/

  route do |r|

      r.get "hello/world" do
        "hello world!"
      end

end
end

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.