Code Monkey home page Code Monkey logo

routr's Introduction

routr

Lifecycle: stable R-CMD-check CRAN_Status_Badge CRAN_Download_Badge Codecov test coverage

routr is a simple and versatile router for R based web servers. For people not familiar with back-end development, a router is a piece of middleware that delegates HTTP requests to the correct handler function. The delegation is based in the URL of the request and in essence means that requests directed at /persons/thomas/ ends up in another handler than /packages/routr/.

routr is heavily inspired by other routers build for other platforms, especially those for Express.js and Ruby on Rails, though it doesn’t mimick either.

Installation

routr is available on CRAN and can be installed in the regular way.

install.packages('routr')

Alternatively you can grab the development version from Github with

# install.packages('devtools')
devtools::install_github('thomasp85/routr')

Functionality

A router is build up of several seperate routes that are collected in a route stack. The stack recieves the request and passes it on to the first route in the stack. Depending on whether the route can handle the request and whether the handler signals a fall-through, the request is passed along the stack until a handler signals that no further processing should be done. This means that it is possible to stack different functionality like user verification, static ressource serving, etc. on top of each other.

The handler

A handler is a function that accepts the arguments request, response, keys, and .... The handler must return a boolean indicating if the request should be passed down the stack (TRUE) or not (FALSE). routr uses the reqres package to provide powerful request and response classes that makes it easy to work with an HTTP exchange. An example of a simple handler is:

h <- function(request, response, keys, ...) {
    response$status <- 200L
    response$type <- 'html'
    response$body <- '<h1>Hello World!</h1>'
    return(FALSE)
}

No matter the content of the request passed to this handler it will return a “Hello World!” to the client. Because it returns FALSE it block any other handlers below it to modify the response.

The route

A route is a collection of handlers. For any given request, only one handler in the route will be called. A route is an object of the R6 Route class and can be created as so:

route <- Route$new()
route$add_handler('get', '/hello/:what/', h)

The first argument to add_handler defines the request type while the second defines the path that the handler responds to. The path need not be static. In the above example the :what defines a variable meaning that the handler will respond to any /hello/<something>/ variation. The variable and the value is available to the handler in the keys argument. For instance, if a request with the URL /hello/mars/ were passed through the route, the keys argument passed to the handler would contain list(what = 'mars'). Variables can only span a single level, meaning that the above handler would not respond to /hello/jupiter/saturn/. To match to anything use /hello/* for responding to any sub-URL to hello. Matches to * will not end up in the keys list. If several paths in a route matches a URL the most specific will be used, meaning that /* will match everything but will always chosen last. With all that in mind lets change the handler to respond to the what variable:

h <- function(request, response, keys, ...) {
    response$status <- 200L
    response$type <- 'html'
    response$body <- paste0('<h1>Hello ', keys$what, '!</h1>')
    return(FALSE)
}
route$add_handler('get', '/hello/:what/', h)

Let’s also add a fallback handler that captures everything:

hFallback <- function(request, response, keys, ...) {
    response$status <- 200L
    response$type <- 'html'
    response$body <- '<h1>I\'m not saying hello to you</h1>'
    return(FALSE)
}
route$add_handler('get', '/*', hFallback)

The route stack

The route stack manages several routes and takes care of receiving a request and returning a response. A route stack is an object of the R6 class RouteStack and is created like this:

router <- RouteStack$new()
router$add_route(route, 'test')

The order in which routes are added to the stack determines the calling order, with those added first taking precedence over those added later. Request are handled by the dispatch method like so:

router$dispatch(request)

Use with fiery

A RouteStack is a fiery-compliant plugin meaning that it can be passed to the attach() method of a fiery server. This will set the server up to pass requests through the route stack and use the resulting response automatically

app <- fiery::Fire$new()
app$attach(router)
app$ignite(block = FALSE)
# In Terminal (or visit in browser)
# curl http://127.0.0.1:8080/hello/mars/
# <h1>Hello Mars!</h1>
app$extinguish()

By default the router responds to request events but can also be used to dispatch on header and message events. In the latter case the request that is send through the handlers is a modified version of the request used to establish the WebSocket version. If used as a WebSocket router a way to extract the path to dispatch on must be provided as part of the RouteStack construction.

Code of Conduct

Please note that the ‘routr’ project is released with a Contributor Code of Conduct. By contributing to this project, you agree to abide by its terms.

routr's People

Contributors

cderv avatar condwanaland avatar cpsievert avatar thomasp85 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

Watchers

 avatar  avatar  avatar  avatar  avatar

routr's Issues

Add method to retrieve a route's handler

Motivation

It can be useful to overwrite a route's handler by first obtaining that handler, then writing a new handler which may call the old handler. For an example, Dash does this to enable an authenticated "superclass" via flask's view_functions property

Implementation

Looking at the current remove_handler() method, perhaps we could add a method that essentially does this?

get_handler = function (method, path) {
    id <- private$find_id(method, path)
    if (is.null(id)) {
        warning("No handler assigned to ", method, " and ", path, 
            call. = FALSE)
    }
  get(id, envir = private$handlerStore)
}

If this sounds useful, I'll take a stab at a PR

add more references to ressource_route(), etc.

hi. i'm converting from Rook::, and was looking for a replacement for its Static and File constructs. i didn't see any, so worked on implementing my own.

i had noticed two lower-case-starting elements of routr::, and had assumed they were uninteresting. indeed, they are not!

so, just a suggestion to mention their existence on the web page, man pages, etc. everywhere you can. maybe in both ?Route and ?RouteStack?

(now to go try to make it work for me... :) cheers.

values in keys are always lower case?

my add_handler call is like
images$add_handler('get', paste(images_url, "/:path", sep=""),
and, in the debugger, i see

Browse[2]> request$path
[1] "/images/IMG_8001.png"
Browse[2]> keys
$path
[1] "img_8001.png"

if it's a feature, maybe add it to the documentation?

(and, i now know, rtfm, that path will always just be one component.)

Release routr 0.3.1

Prepare for release:

  • devtools::check()
  • devtools::check_win_devel()
  • rhub::check_for_cran()
  • revdepcheck::revdep_check(num_workers = 4)
  • Polish NEWS
  • Polish pkgdown reference index

Submit to CRAN:

  • usethis::use_version('patch')
  • Update cran-comments.md
  • devtools::submit_cran()
  • Approve email

Wait for CRAN...

  • Accepted 🎉
  • usethis::use_github_release()
  • usethis::use_dev_version()
  • Tweet

Relaying R (handler) errors to R console

Looks as though (route handler) errors are relayed in the response to the client (in the body), but I could see use cases where you may also/rather want to relay that error to the R console (makes debugging easier, if nothing else)

library(routr)
library(fiery)

route <- Route$new()
route$add_handler("get", "/", function(request, response, keys, ...) {
  message("Handler was triggered ")
  stop("Oops, something went wrong", call. = FALSE)
  TRUE
})

router <- RouteStack$new()
router$add_route(route, "name")

app <- Fire$new()
app$attach(router)
app$ignite(showcase = TRUE)

fake_request is not a Request

looks like

a) routr:::Request is not exported, so I have to routr:::Request$new() to make one
b) fake_request does not actually inherit from Request, so trivial_router$dispatch( fake_request('whatever') ) fails

Nice package!

Bug in route remapping

library(routr)

r <- Route$new()

r$add_handler("get", "/", function(request, response, keys, ...) {
  response$status <- 200L
  response$body <- "All is good"
  return(FALSE)
})

r$remap_handlers(function(method, path, handler) {
  r$add_handler(method, paste0('/prefix', path), handler)
})
Error in args %in% names(formals(f)) : 
  argument "args" is missing, with no default

s/reciev/receiv/g

thanks so much for fiery/reqres/routr.

there's a persistent misspelling. the rule we were taught in the US early on is: "'I' before 'E' except after 'C'".

it being English, there are exceptions. in the code/docs, "dependencies", "policies" are correct.

cheers. (i'm making the issue here in routr, but the same is true in fiery; reqres gets a "pass" in spelling. :)

routr / plumber comparison

Hi Thomas - not an issue but I wonder if you have any views on how routr and plumber sit with respect to each other (assuming you're familiar with the latter)? It's difficult from a starter perspective to know what the relative strengths are, and if/how they might be complementary.

Issue naming RouteStack

Perhaps I'm doing it wrong, but:

> library(routr)
> router <- RouteStack$new()
> router$name <- "some name"
Error in (function ()  : unused argument (quote("some name"))

ressource_route: if default file used, content-type incorrect?

if the default file is used (because the path ends in '/'), it appears that the file extension isn't set, and the file is returned, but with an incorrect 'content-type' (i assume it is):

bash % wget -v http://127.0.0.1:8080
--2020-11-09 20:22:48--  http://127.0.0.1:8080/
Connecting to 127.0.0.1:8080... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1321 (1.3K) [text/plain]
Saving to: ‘index.html.2’

index.html.2                                    100%[======================================================================================================>]   1.29K  --.-KB/s    in 0s

2020-11-09 20:22:48 (136 MB/s) - ‘index.html.2’ saved [1321/1321]

this patch, which sets the default file earlier in the handler, seems to fix that. (what it breaks, ...?)

diff --git a/R/ressource_route.R b/R/ressource_route.R
index 549d856..02021fe 100644
--- a/R/ressource_route.R
+++ b/R/ressource_route.R
@@ -109,13 +109,13 @@ ressource_route <- function(..., default_file = 'index.html', default_ext = 'htm
   assert_that(has_attr(mappings, 'names'))
   route$add_handler('get', '/*', function(request, response, keys, ...) {
     path <- request$path
+    if (grepl('/$', path)) path <- paste0(path, default_file)
     file_extension <- file_ext(path)
     has_ext <- file_extension != ''
     found <- FALSE
     file <- NA
     enc <- NA
     real_file <- NA
-    if (grepl('/$', path)) path <- paste0(path, default_file)
     for (i in seq_along(mappings)) {
       mount <- names(mappings)[i]
       if (!grepl(paste0('^', mount), path)) next

with this patch, here is how wget runs

bash % wget http://127.0.0.1:8080
--2020-11-09 20:39:15--  http://127.0.0.1:8080/
Connecting to 127.0.0.1:8080... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1321 (1.3K) [text/html]
Saving to: ‘index.html.3’

index.html.3                                    100%[======================================================================================================>]   1.29K  --.-KB/s    in 0s

2020-11-09 20:39:15 (126 MB/s) - ‘index.html.3’ saved [1321/1321]

(this seems to make the browser launched by fiery::Fire$ignite()'s showcase work, at least for me.)

Release routr 0.4.1

Prepare for release:

  • git pull
  • Check current CRAN check results
  • Polish NEWS
  • devtools::build_readme()
  • urlchecker::url_check()
  • devtools::check(remote = TRUE, manual = TRUE)
  • devtools::check_win_devel()
  • rhub::check_for_cran()
  • revdepcheck::revdep_check(num_workers = 4)
  • Update cran-comments.md
  • git push

Submit to CRAN:

  • usethis::use_version('patch')
  • devtools::submit_cran()
  • Approve email

Wait for CRAN...

  • Accepted 🎉
  • git push
  • usethis::use_github_release()
  • usethis::use_dev_version()
  • git push

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.