diff --git a/.impt.test b/.impt.test
index 0ffafbb..a6ea062 100644
--- a/.impt.test
+++ b/.impt.test
@@ -9,5 +9,5 @@
"*.test.nut",
"tests/**/*.test.nut"
],
- "agentFile": "Rocky.class.nut"
-}
\ No newline at end of file
+ "agentFile": "Rocky.agent.lib.nut"
+}
diff --git a/LICENSE b/LICENSE
index 974a8d1..b77deb6 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2015 Electric Imp
+Copyright (c) 2015-19 Electric Imp
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Middleware-Example/middlewareExample.agent.nut b/Middleware-Example/middlewareExample.agent.nut
index decf6a2..e427117 100644
--- a/Middleware-Example/middlewareExample.agent.nut
+++ b/Middleware-Example/middlewareExample.agent.nut
@@ -1,4 +1,8 @@
-#require "rocky.class.nut:2.0.0"
+// Copyright (c) 2015-19 Electric Imp
+// This file is licensed under the MIT License
+// http://opensource.org/licenses/MIT
+
+#require "Rocky.agent.lib.nut:3.0.0"
// Dummy Data for API
data <- { "foo": "bar" };
@@ -17,7 +21,7 @@ function debugMiddleware(context, next) {
// Middleware to add CORS headers
function CORSMiddleware(context, next) {
- server.log("Adding CORS headers to request")
+ server.log("Adding CORS headers to request");
// Add some headers
context.setHeader("Access-Control-Allow-Origin", "*");
@@ -29,7 +33,7 @@ function CORSMiddleware(context, next) {
}
// Setup Rocky and use the debugMiddleware on ALL requests
-app <- Rocky().use([ debugMiddleware ]);
+app <- Rocky.init().use([ debugMiddleware ]);
// GET / - send hello world
app.get("/", function(context) {
diff --git a/README.md b/README.md
index acaa11f..b6ec05d 100644
--- a/README.md
+++ b/README.md
@@ -1,84 +1,141 @@
-# Rocky 2.0.2
+# Rocky 3.0.0 #
-Rocky is an framework for building powerful and scalable APIs for your imp-powered products. The Rocky library consists of the following classes:
+Rocky is an framework for building powerful and scalable APIs for your imp-powered products.
+
+**Important** From version 3.0.0, Rocky is implemented as a table rather than a class. **This is a breaking change**. This change has been made to ensure that Rocky is available solely as a singleton. For full details on updating your code, please see [**Rocky Usage**](#rocky-usage), below.
![Build Status](https://cse-ci.electricimp.com/app/rest/builds/buildType:(id:Rocky_BuildAndTest)/statusIcon)
----
+The Rocky library consists of the following components:
- [Rocky](#rocky) — The core application, used to create routes, set default handlers, etc.
- - [Rocky.get](#rocky_verb) — Creates a handler for GET requests that match the specified signature.
- - [Rocky.put](#rocky_verb) — Creates a handler for PUT requests that match the specified signature.
- - [Rocky.post](#rocky_verb) — Creates a handler for POST requests that match the specified signature.
- - [Rocky.on](#rocky_on) — Creates a handler for requests that match the specified verb and signature.
- - [Rocky.use](#rocky_use) — Binds one or more middlewares to all routes.
- - [Rocky.authorize](#rocky_authorize) — Specify the default `authorize` handler for all routes.
- - [Rocky.onUnauthorized](#rocky_onunauthorized) — Specify the default `onUnauthorized` callback for all routes.
- - [Rocky.onTimeout](#rocky_ontimeout) — Set the default `onTimeout` handler for all routes.
- - [Rocky.onNotFound](#rocky_onnotfound) — Set the default `onNotFound` handler for all routes.
- - [Rocky.onException](#rocky_onexception) — Set the default `onException` handler for all routes.
- - [Rocky.getContext](#rocky_getcontext) — Static method that retrieves a [Rocky.Context](#context) object by it's ID (primairly used for asyncronous requests).
- - [Rocky.sendToAll](#rocky_sendtoall) — Static method that sends a response to *all* open requests/requests.
+ - *Singleton Methods*
+ - [Rocky.init()](#rocky_init) — Initializes the singleton and prepares it for use.
+ - [Rocky.get()](#rocky_verb) — Creates a handler for GET requests that match the specified signature.
+ - [Rocky.put()](#rocky_verb) — Creates a handler for PUT requests that match the specified signature.
+ - [Rocky.post()](#rocky_verb) — Creates a handler for POST requests that match the specified signature.
+ - [Rocky.on()](#rocky_on) — Creates a handler for requests that match the specified verb and signature.
+ - [Rocky.use()](#rocky_use) — Binds one or more middlewares to all routes.
+ - [Rocky.authorize()](#rocky_authorize) — Specify the default `authorize` handler for all routes.
+ - [Rocky.onUnauthorized()](#rocky_onunauthorized) — Specify the default `onUnauthorized` callback for all routes.
+ - [Rocky.onTimeout()](#rocky_ontimeout) — Set the default `onTimeout` handler for all routes.
+ - [Rocky.onNotFound()](#rocky_onnotfound) — Set the default `onNotFound` handler for all routes.
+ - [Rocky.onException()](#rocky_onexception) — Set the default `onException` handler for all routes.
+ - [Rocky.getContext()](#rocky_getcontext) — Retrieve a [Rocky.Context](#context) object by its ID.
+ - [Rocky.sendToAll()](#rocky_sendtoall) — Send a response to *all* open requests.
- [Rocky.Route](#route) — A handler for a specific route.
- - [Rocky.Route.use](#route_use) -— Binds one or more middlewares to the route.
- - [Rocky.Route.authorize](#route_authorize) — Specify the default `authorize` handler for the route.
- - [Rocky.Route.onUnauthorized](#route_onunauthorized) - Specify the default `onUnauthorized` callback for the route.
- - [Rocky.Route.onTimeout](#route_ontimeout) — Set the default `onTimeout` handler for the route.
- - [Rocky.Route.onException](#route_onexception) - Set the default `onException` handler for the route.
+ - *Instance Methods*
+ - [Rocky.Route.use()](#route_use) -— Binds one or more middlewares to the route.
+ - [Rocky.Route.authorize()](#route_authorize) — Specify the default `authorize` handler for the route.
+ - [Rocky.Route.onUnauthorized()](#route_onunauthorized) — Specify the default `onUnauthorized` callback for the route.
+ - [Rocky.Route.onTimeout()](#route_ontimeout) — Set the default `onTimeout` handler for the route.
+ - [Rocky.Route.onException()](#route_onexception) — Set the default `onException` handler for the route.
+ - [Rocky.Route.hasHandler()](#route_hashandler) — Determine if the route has a named handler registered.
+ - [Rocky.Route.getHandler()](#route_gethandler) — Get a named handler.
+ - [Rocky.Route.getTimeout()](#route_gettimeout) — Retrieve the current route-specific timeout setting.
+ - [Rocky.Route.setTimeout()](#route_settimeout) — Set a new route-level timeout.
- [Rocky.Context](#context) - The information passed into a route handler.
- - [Rocky.Context.send](#context_send) — Sends an HTTP response.
- - [Rocky.Context.isComplete](#context_iscomplete) — Returns whether a response has been sent for the current context.
- - [Rocky.Context.getHeader](#context_getheader) — Attempts to get the specified header from the request object.
- - [Rocky.Context.setHeader](#context_setheader) — Sets the specified header in the response object.
- - [Rocky.Context.req](#context_req) — The HTTP Request Table.
- - [Rocky.Context.id](#context_id) — Context's unique ID.
- - [Rocky.Context.userdata](#context_userdata) — Field developers can use to store data during long running tasks, etc
- - [Rocky.Context.path](#context_path) — The full path the request was made to.
- - [Rocky.Context.matches](#context_matches) — An array of matches to the path's regular expression.
- - [Rocky.Context.isBrowser](#context_isbrowser) — Returns true if the request contains an `Accept: text/html` header.
- - [Rocky.Context.sendToAll](#context_sendtoall) — Static method that sends a response to *all* open requests/contexts.
+ - *Instance Methods*
+ - [Rocky.Context.send()](#context_send) — Sends an HTTP response.
+ - [Rocky.Context.isComplete()](#context_iscomplete) — Returns whether a response has been sent for the current context.
+ - [Rocky.Context.getHeader()](#context_getheader) — Attempts to get the specified header from the request object.
+ - [Rocky.Context.setHeader()](#context_setheader) — Sets the specified header in the response object.
+ - [Rocky.Context.setTimeout()](#context_settimeout) — Set a context timeout.
+ - [Rocky.Context.isBrowser()](#context_isbrowser) — Returns `true` if the request contains an `Accept: text/html` header.
+ - *Class Methods*
+ - [Rocky.Context.get()](#context_get) — Class method that can retrieve a specific context.
+ - [Rocky.Context.sendToAll()](#context_sendtoall) — Class method that sends a response to *all* open requests/contexts.
+ - *Properties*
+ - [Rocky.Context.req](#context_req) — The HTTP Request Table.
+ - [Rocky.Context.id](#context_id) — The context's unique ID.
+ - [Rocky.Context.path](#context_path) — The full path the request was made to.
+ - [Rocky.Context.matches](#context_matches) — An array of matches to the path's regular expression.
+ - [Rocky.Context.userdata](#context_userdata) — A field developers can use to store data during long running tasks, etc
- [Middleware](#middleware) - Used to transform and verify data before the main request handler.
- [Order of Execution](#middleware_orderofexecution) — Explanation of the execution flow for middleware and event handlers.
- [CORS Requests](#cors_requests) — How to handle cross-site HTTP requests ([CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)).
-
Rocky([options])
+
+
+## Rocky Usage ##
+
+Rocky 3.0.0 is implemented as a table to enforce singleton behavior. You code should no longer instantiate Rocky using a constructor call, but instead call the new *init()* method to initialize the library.
-Calling the Rocky constructor creates a new Rocky application. An optional *options* table can be passed into the constructor to override default behaviours:
+All of Rocky’s methods are accessible as before, and return the same values. *init()* returns a reference to the Rocky singleton. There is no longer a distinction between class and instance methods: all of Rocky’s methods can be called on Rocky itself, or an alias variables, as these reference the same table:
```squirrel
-#require "rocky.class.nut:2.0.2"
+// These calls are equivalent
+app.get("/users/([^/]*)", function(context) {
+ local username = context.matches[1];
+});
-app <- Rocky()
+Rocky.get("/users/([^/]*)", function(context) {
+ local username = context.matches[1];
+});
```
-### options
+**Note** [Rocky.Context](#context) and [Rocky.Route](#route) continue to be implemented as classes, but remember that you will not be creating instances of these classes yourself — new instances will be made available to you as needed, by Rocky.
+
+## Rocky Methods ##
+
+init([settings])
-An table containing any of the following keys may be passed into the Rocky constructor to modify the default behaviour:
+The new *init()* method takes the same argument as the former constructor: an optional [table of settings](#initialization-options).
-- *accessControl* — Modifies whether or not Rocky will automatically add `Access-Control` headers to the response object
-- *allowUnsecure* — Modifies whether or not Rocky will accept HTTP requests (as opposed to HTTPS)
-- *strictRouting* — Enables or disables strict routing. By default, Rocky will consider `/foo` and `/foo/` as identical paths.
-- *timeout* — Modifies how long Rocky will hold onto a request before automatically executing the *onTimeout* handler
+Even if your code doesn’t alter Rocky’s default behavior, you still need to call *init()* in order to ensure that the table is correctly initialized for use. If you call *init()* again, the default settings and event handlers will be re-applied.
-These are the default settings:
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *settings* | Table | No | See [**Initialization Options**](#initialization-options) for details and setting defaults |
+
+#### Initialization Options ####
+
+A table containing any of the following keys may be passed into *init()* to modify the library’s default behavior:
+
+| Key | Description |
+| --- | --- |
+| *accessControl* | Modifies whether Rocky will automatically add `Access-Control` headers to the response object. Default: `true` |
+| *allowUnsecure* | Modifies whether Rocky will accept HTTP requests. Default: `false` (ie. HTTPS only) |
+| *strictRouting* | Enables or disables strict routing. Default: `false` (ie. Rocky will consider `/foo` and `/foo/` to be identical) |
+| *sigCaseSensitive* | Enforce [signature](#signatures) case sensitivity. Default: `false` (ie. Rocky will consider `/FOO` and `/foo` to be identical) |
+| *timeout* | Modifies how long Rocky will hold onto a request before automatically executing the *onTimeout* handler. Default: 10s |
+
+#### Example ####
```squirrel
-defaults <- {
- accessControl = true,
- allowUnsecure = false,
- strictRouting = false,
- timeout = 10
-}
+#require "rocky.agent.lib.nut:3.0.0"
+
+local settings = { "timeout": 30 };
+app <- Rocky.init(settings);
```
VERB(signature, callback[, timeout])
-The *VERB()* methods allow you to assign routes based on the specified verb and signature. The following *VERB*s are allowed:
+
+Rocky’s *VERB()* methods allow you to assign routes based on the specified verb and [signature](#signatures). The following *VERB()* methods are provided:
- app.get(*signature, callback[, timeout]*)
- app.put(*signature, callback[, timeout]*)
- app.post(*signature, callback[, timeout]*)
-When a match is found on the verb (as specified by the method) and the *signature*, the callback function will be executed. The callback takes a [Rocky.Context](#context) object as a parameter. An optional route-level timeout can be passed in. If no timeout is passed in, the timeout set in the constructor will be used.
+When a match is found on the verb (as specified by the method) and the signature, the callback function will be executed. The callback receives a [Rocky.Context](#context) object as its only argument.
+
+An optional route-level timeout can be specified. If no timeout is specified, the timeout set in [the initializer](#rocky_init) will be used.
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *signature* | String | Yes | A [signature](#signatures) defining the API endpoint |
+| *callback* | Function | Yes | A function to handle the request. It receives a [Rocky.Context](#context) object |
+| *timeout* | String | No | An optional request timeout in seconds. Default: the global default or [*init()*](#rocky_init)-applied timeout |
+
+#### Returns ####
+
+Rocky.Route — an instance representing the registered handler.
+
+#### Example ####
```squirrel
// Responds with '200, { "message": "hello world" }'
@@ -88,30 +145,24 @@ app.get("/", function(context) {
})
```
-Signatures
+on(verb, signature, callback[, timeout])
-Signatures can either be fully qualified paths (`/led/state`) or include regular expressions (`/users/([^/]*)`). If the path is specified using a regular expressions, any matches will be added to the [Rocky.Context](#context) object passed into the callback. In the following example, we capture the desired user’s username:
+This method allows you to create APIs that use verbs other than GET, PUT or POST. The *on()* method works identically to the *VERB()* methods, but you specify the verb as a string.
-```squirrel
-// Get a user
-app.get("/users/([^/]*)", function(context) {
- // Grab the username from the regex
- // (context.matches[0] will always be the full path)
- local username = context.matches[1];
+#### Parameters ####
- if (username in usersTable) {
- // If we found the user, return the user object
- context.send(usersTable[username]);
- } else {
- // If the user doesn't exist, return a 404
- context.send(404, { "error": "Unknown User" });
- }
-});
-```
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *verb* | String | Yes | The HTTP request verb |
+| *signature* | String | Yes | A [signature](#signatures) defining the API endpoint |
+| *callback* | Function | Yes | A function to handle the request. It receives a [Rocky.Context](#context) object |
+| *timeout* | String | No | An optional request timeout in seconds. Default: the global default or [*init()*](#rocky_init)-applied timeout |
-on(verb, signature, callback[, timeout])
+#### Returns ####
+
+Rocky.Route — an instance representing the registered handler.
-The *on()* method allows you to create APIs that use verbs other than GET, PUT or POST. The *on()* method works identically to the *VERB()* methods, but you specify the verb as a string:
+#### Example ####
```squirrel
// Delete a user
@@ -133,7 +184,21 @@ app.on("delete", "/users/([^/]*)", function(context) {
use(callback)
-The *use()* method allows you to attach a middleware, or array of middlewares, to the global Rocky object.
+This method allows you to attach an application-specific handler, called a “middleware” function, or an array of middleware functions, to the global Rocky object. Please see [**Middleware**](#middleware) for more information.
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *callback* | Function or array of functions | Yes | One or more [middleware functions](#middleware) |
+
+The callback function receives a [Rocky.Context](#context) object and a reference to the next middleware in sequence as its arguments. See the example below for guidance.
+
+#### Returns ####
+
+*this* — the target Rocky instance.
+
+#### Example ####
```squirrel
// Create a function to add the specific CORS headers we want:
@@ -146,7 +211,7 @@ function customCORSMiddleware(context, next) {
next();
}
-app <- Rocky({ "accessControl": false });
+app <- Rocky.init({ "accessControl": false });
// Add the middleware to the global Rocky object so every
// incoming request has the headers added
@@ -157,16 +222,26 @@ app.get("/", function(context) {
});
```
-See the [Middleware](#middleware) section for more information.
-
authorize(callback)
-The *authorize()* method allows you to specify a global function to validate or authorize incoming requests. The callback function takes a [Rocky.Context](#context) object as a parameter, and must return either `true` (if the request is authorized) or `false` (if the request is not authorized).
+This method allows you to specify a global function to validate or authorize incoming requests.
-The *authorize()* method is executed before the main request handler.
+#### Parameters ####
-- If the callback return `true`, the route handler will be invoked.
-- If the callback returns `false`, the [onUnauthorized](#rocky_onAuthorized) response handler is invoked.
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *callback* | Function | Yes | A function to authorize or reject the request |
+
+The callback function takes a [Rocky.Context](#context) object as its single argument and must return either `true` (if the request is authorized) or `false` (if the request is not authorized). The callback is executed before the main request handler, so:
+
+- If the callback returns `true`, the route handler will be invoked.
+- If the callback returns `false`, the [onUnauthorized](#rocky_onunauthorized) response handler is invoked.
+
+#### Returns ####
+
+*this* — the target Rocky instance.
+
+#### Example ####
```squirrel
app.authorize(function(context) {
@@ -177,7 +252,21 @@ app.authorize(function(context) {
onUauthorized(callback)
-The *onUnauthorized()* method allows you to configure the default response to requests that fail the *authorize()* method. The callback method takes a [Rocky.Context](#context) object as a parameter. The callback method passed into *onUnauthorized()* will be executed for all unauthorized requests that do not have a route-level [onUnauthorized](route_onUnauthorized) response handler.
+This method allows you to configure the default response to requests that fail to be authorized via the callback registered with [*authorize()*](#rocky_authorize).
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *callback* | Function | Yes | A function to manage unauthorized requests |
+
+The callback takes a [Rocky.Context](#context) object as its single argument and will be executed for all unauthorized requests that do not have a route-level [onUnauthorized](#route_onunauthorized) response handler.
+
+#### Returns ####
+
+*this* — the target Rocky instance.
+
+#### Example ####
```squirrel
app.onUnauthorized(function(context) {
@@ -185,9 +274,24 @@ app.onUnauthorized(function(context) {
});
```
-onTimeout(callback)
+onTimeout(callback, [timeout])
+
+This method allows you to configure the default response to requests that exceed the timeout.
+
+#### Parameters ####
-The *onTimeout()* method allows you to configure the default response to requests that exceed the timeout. The callback method passed into *onTimeout()* will be executed for all timed out requests that do not have a route-level [onTimeout](route_onTimeout) response handler. The callback method takes a [Rocky.Context](#context) object as a parameter. This method should (but is not required to) send a response code of 408.
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *callback* | Function | Yes | A function to manage requests that timed out |
+| *timeout* | Float or integer | No | Optional timeout in seconds. Default: 10s |
+
+The callback takes a [Rocky.Context](#context) object as its single argument and will be executed for all timed out requests that do not have a route-level [onTimeout](#route_onTimeout) response handler. The callback should (but is not required to) send a response code of 408.
+
+#### Returns ####
+
+*this* — the target Rocky instance.
+
+#### Example ####
```squirrel
app.onTimeout(function(context) {
@@ -197,29 +301,73 @@ app.onTimeout(function(context) {
onNotFound(callback)
-The *onNotFound()* method allows you to configure the response handler for requests that could not match a route. The callback method takes a [Rocky.Context](#context) object as a parameter. This method should (but is not required to) send a response code of 404.
+This method allows you to configure the response handler for requests that could not match a route.
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *callback* | Function | Yes | A function to manage requests that could not be matched to an endpoint |
+
+The callback takes a [Rocky.Context](#context) object as its single argument. It should (but is not required to) send a response code of 404.
+
+#### Returns ####
+
+*this* — the target Rocky instance.
+
+#### Example ####
```squirrel
app.onNotFound(function(context) {
- context.send(404, { "message": "Oh snaps, the resource you're looking for doesn't exist!" });
+ context.send(404, { "message": "The resource you're looking for doesn't exist" });
});
```
onException(callback)
-The *onException()* method allows you to configure the global response handler for requests that encounter runtime errors. The callback method takes two parameters: a [Rocky.Context](#context) object and the exception. The callback method will be executed for all requests that encounter runtime errors and do not have a route-level [onException](route_onexception) handler. This method should (but is not required to) send a response code of 500.
+This method allows you to configure the global response handler for requests that encounter runtime errors.
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *callback* | Function | Yes | A function to manage requests that triggered runtime errors |
+
+The callback takes a [Rocky.Context](#context) object and the exception as its arguments. See the example below for usage guidance. It will be executed for all requests that encounter runtime errors and do not have a route-level [onException](#route_onexception) handler. This method should (but is not required to) send a response code of 500.
+
+#### Returns ####
+
+*this* — the target Rocky instance.
+
+#### Example ####
```squirrel
-app.onException(function(context, ex) {
- context.send(500, { "message": "Internal Agent Error", "error": ex });
+app.onException(function(context, except) {
+ context.send(500, { "message": "Internal Agent Error",
+ "error": except });
});
```
-Rocky.getContext(id)
+getContext(id)
+
+Every [Rocky.Context](#context) object created by Rocky is assigned a unique ID that can retrieved by reading its [context.id](#context_id) field. Pass such an ID into *getContext()* to retrieve previously created contexts. This method is primarily used for long-running or asynchronous requests.
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *id* | String | Yes | The ID of the required context |
-Every [Rocky.Context](#context) object created by Rocky is assigned a unique ID that can found using [context.id](#context_id). We can use this ID and the static *getContext()* method to retrieve previously created contexts. This is primarily used for long-running or asynchronous requests. In the following example, we fetch the temperature from the device when the request is made:
+#### Returns ####
+
+Nothing.
+
+#### Example ####
+
+In this example, we fetch the temperature from the device when the request is made.
```squirrel
+// Agent Code
app.get("/temp", function(context) {
// Send a getTemp request to the device, and pass context.id as the data
device.send("getTemp", context.id);
@@ -227,7 +375,7 @@ app.get("/temp", function(context) {
device.on("getTempResponse", function(data) {
// When we get a getTempResponse message, get the context
- local context = Rocky.getContext(data.id);
+ local context = app.getContext(data.id);
// then send the response using that context
if (!context.isComplete()) {
@@ -237,7 +385,7 @@ device.on("getTempResponse", function(data) {
```
```squirrel
-// device code
+// Device Code
agent.on("getTemp", function(id) {
local temp = getTemp();
// When we get a "getTemp" message, send back a response that includes
@@ -246,9 +394,23 @@ agent.on("getTemp", function(id) {
});
```
-Rocky.sendToAll(statuscode, response[, headers])
+sendToAll(statuscode, response[, headers])
-The static *sendToAll()* method sends a response to **all** open requests. This is most useful in APIs that allow for long-polling.
+This method sends a response to **all** open requests. This is most useful in APIs that allow for long-polling.
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *statuscode* | Integer | Yes | The response’s [HTTP status code](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes) |
+| *response* | String | Yes | The response’s body |
+| *headers* | Table | No | Additional response headers and their values. Default: no extra headers |
+
+#### Returns ####
+
+Nothing.
+
+#### Example ####
```squirrel
app.get("/poll", function(context) {
@@ -257,15 +419,15 @@ app.get("/poll", function(context) {
// When we get data - send it to all open requests
device.on("data", function(data) {
- Rocky.sendToAll(200, data);
+ app.sendToAll(200, data);
});
```
-Rocky.Route
+Rocky.Route Methods
-The Rocky.Route object encapsulates the behaviour associated with a request made to a specific route. You should never call the Rocky.Route constructor directly, instead, you should create and associate routes using the [*Rocky.get()*](#rocky_get), [*Rocky.put()*](#rocky_put), [*Rocky.post()*](rocky_post) and [*Rocky.on()*](rocky_on) methods.
+The Rocky.Route object encapsulates the behavior associated with a request made to a specific route. You should never call the Rocky.Route constructor directly; instead, create and associate routes using Rocky’s [*get()*](#rocky_verb), [*put()*](#rocky_verb), [*post()*](#rocky_verb) and [*on()*](#rocky_on) methods.
-All methods that affect the behaviour of a route are designed to be used in a fluent style, ie. the methods return the route object itself, so they can be chained together.
+All methods that affect the behavior of a route are designed to be used in a fluent style, ie. the methods return the route object itself, so they can be chained together. For example:
```squirrel
app.get("/", function(context) {
@@ -279,10 +441,24 @@ app.get("/", function(context) {
use(callback)
-The *use()* method allows you to attach a middleware, or array of middlewares, to a specific route.
+This method allows you to attach a middleware function, or an array of middleware functions, to a specific route. Please see [**Middleware**](#middleware) for more information.
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *callback* | Function or array of functions | Yes | One or more [middleware functions](#middleware) |
+
+The callback function receives a [Rocky.Context](#context) object and a reference to the next middleware in sequence as its arguments. See the example below for guidance.
+
+#### Returns ####
+
+Nothing.
+
+#### Example ####
```squirrel
-app <- Rocky();
+app <- Rocky.init();
// Custom Middleware to validate new users
function validateNewUserMiddleware(context, next) {
@@ -305,16 +481,26 @@ app.post("/users", function(context) {
}).use([ validateNewUserMiddleware ]);
```
-See the [Middleware](#middleware) section for more information.
-
authorize(callback)
-The *authorize()* method allows you to specify a route-level function to validate or authorize incoming requests. A route-level authorize handler will override the global authorize handler set by [*Rocky.authorize()*](#rocky_authorize) for requests made to the specified route. The callback function takes a [Rocky.Context](#context) object as a parameter, and must return either `true` (if the request is authorized) or `false` (if the request is not authorized).
+This method allows you to specify a route-level function to validate or authorize incoming requests. Such a route-level authorization handler will override the global authorization handler set by Rocky’s [*authorize()*](#rocky_authorize) method for requests made to the specified route.
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *callback* | Function | Yes | A function to authorize or reject the request |
-The *authorize()* method is executed before the main request handler.
+The callback function receives a [Rocky.Context](#context) object as its single argument and must return either `true` (if the request is authorized) or `false` (if the request is not authorized). The callback is executed before the main request handler, so:
- If the callback returns `true`, the route handler will be invoked.
-- If the callback returns `false`, the [onUnauthorized](#rocky_onAuthorized) handler is invoked.
+- If the callback returns `false`, the route-specific [onUnauthorized](#route_onunauthorized) response handler is invoked. If there is no route-specific [onUnauthorized](#route_onunauthorized) response handler, the global [onUnauthorized](#rocky_onunauthorized) response handler is invoked.
+
+#### Returns ####
+
+Nothing.
+
+#### Example ####
```squirrel
// Delete a user
@@ -331,7 +517,21 @@ app.on("delete", "/users/([^/]*)", function(context) {
onUauthorized(callback)
-The *onUnauthorized()* method allows you to configure a route -level response to requests that fail the *authorize()* method. A route-level onUnauthorized handler will override the global onUnauthorized handler set by [*Rocky.onUnauthorized()*](#rocky_onunauthorized) for requests made to the specified route. The callback method takes a [Rocky.Context](#context) object as a parameter. The callback method passed into *onUnauthorized()* will be executed for all unauthorized requests that do not have a route-level [onUnauthorized](route_onUnauthorized) response handler.
+This method allows you to configure a route-level response to requests that fail the route-specific authorization handler, if present. A route-level onUnauthorized handler will override the global onUnauthorized handler set by Rocky’s [*onUnauthorized()*](#rocky_onunauthorized) method for requests made to the specified route.
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *callback* | Function | Yes | A function to authorize or reject the request |
+
+The callback function receives a [Rocky.Context](#context) object as its single argument and will be executed for all unauthorized requests made to the specified route.
+
+#### Return Value ####
+
+Nothing.
+
+#### Example ####
```squirrel
// Delete a user
@@ -350,7 +550,21 @@ app.on("delete", "/users/([^/]*)", function(context) {
onTimeout(callback)
-The *onTimeout()* method allows you to configure a route level response to requests that exceed the timeout. A route-level onTimeout handler will override the global onTimeout handler set by [*Rocky.onTimeout()*](#rocky_ontimeout) for requests made to the specified route. The callback method passed into *onTimeout()* will be executed for all timed out requests that do not have a route-level [onTimeout](route_onTimeout) response handler. The callback method takes a [Rocky.Context](#context) object as a parameter. This method should (but is not required to) send a response code of 408.
+This method allows you to configure a route-level response to requests that exceed the timeout. A route-level onTimeout handler will override the global onTimeout handler set by Rocky’s [*onTimeout()*](#rocky_ontimeout) method for requests made to the specified route.
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *callback* | Function | Yes | A function to manage requests that timed out |
+
+The callback takes a [Rocky.Context](#context) object as its single argument and will be executed for all timed out requests made to the specified route. The callback should (but is not required to) send a response code of 408.
+
+#### Returns ####
+
+Nothing.
+
+#### Example ####
```squirrel
app.get("/", function(context) {
@@ -369,7 +583,21 @@ device.on("getTempResponse", function(data) {
onException(callback)
-The *onException()* method allows you to configure a route-level response handler for requests that encounter runtime errors. A route-level onException handler will override the global onException handler set by [*Rocky.onTimeout()*](#rocky_onexception) for requests made to the specified route. The callback method takes two parameters: a [Rocky.Context](#context) object and the exception. The callback method will be executed for all requests that encounter runtime errors and do not have a route-level [onException](route_onexception) handler. This method should (but is not required to) send a response code of 500.
+This method allows you to configure a route-level response handler for requests that encounter runtime errors. A route-level onException handler will override the global onException handler set by Rocky’s [*onTimeout()*](#rocky_ontimeout) method for requests made to the specified route.
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *callback* | Function | Yes | A function to manage requests that triggered runtime errors |
+
+The callback takes a [Rocky.Context](#context) object and the exception as its arguments. See the example below for usage guidance. It will be executed for all requests made to the specified route that encounter runtime errors. This method should (but is not required to) send a response code of 500.
+
+#### Returns ####
+
+Nothing.
+
+#### Example ####
```squirrel
app.get("/", function(context) {
@@ -380,20 +608,80 @@ app.get("/", function(context) {
});
```
-Rocky.Context
+hasHandler(handlerName)
+
+This method allows you to check whether a specific handler has been set for a given [Rocky.Route](#route) instance.
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *handlerName* | String | Yes | The requested handler’s name |
+
+#### Returns ####
+
+Boolean — `true` if the named handler has been registered, otherwise `false`.
+
+getHandler(handlerName)
+
+This method allows you to retrieve a specific handler by its name.
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *handlerName* | String | Yes | The requested handler’s name |
+
+#### Returns ####
+
+Function — The named handler, otherwise `null`.
+
+execute(handlerName)
-The Rocky.Context object encapsulates an [HTTP Request Table](http://electricimp.com/docs/api/httphandler/) an [HTTPResponse](http://electricimp.com/docs/api/httpresponse/) object, and other important information. When a request is made, Rocky will automatically generate a new context object for that request and pass it to the required callbacks, ie. you should never manually create a Rocky.Context object.
+getTimeout()
+
+This method allows you to retrieve the current route-specific timeout setting.
+
+#### Returns ####
+
+Float — The current timeout value.
+
+setTimeout(timeout)
+
+This method allows you to specify a new route-level timeout setting.
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *timeout* | Float or integer | Yes | The new timeout setting |
+
+#### Returns ####
+
+Float — The new timeout value.
+
+Rocky.Context Instance Methods
+
+A Rocky.Context object encapsulates an [HTTP Request Table](https://developer.electricimp.com/api/httprequest), an [HTTPResponse](https://developer.electricimp.com/api/httpresponse) object, and other important information. When a request is made, Rocky will automatically generate a new context object for that request and pass it to the required callbacks. Never manually create a Rocky.Context object.
send(statuscode[, message])
+This method returns a response to a request made to a Rocky application.
-The *send()* method returns a response to a request made to a Rocky application. It takes two parameters. The first is an integer [HTTP status code](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes). The second parameter, which is optional, is the data that will be relayed back to the requester, either a string, an array of values or a table.
+#### Parameters ####
-**Note** Arrays and tables are automatically JSON-encoded before being sent.
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *statuscode* | Integer | Yes | The response’s [HTTP status code](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes) |
+| *message* | String, array or table | No | The response’s body. Arrays and tables are automatically JSON-encoded before being sent |
-The method returns `false` if the context has already been used to respond to the request.
+#### Return Value ####
-```
+Boolean — `false` if the context has already been used to respond to the request, otherwise `true`.
+
+#### Examples ####
+
+```squirrel
app.get("/color", function(context) {
context.send(200, { "color": led.color })
})
@@ -401,7 +689,19 @@ app.get("/color", function(context) {
send(message)
-The *send()* method may also be invoked without a status code. When invoked in this fashion, a status code of 200 is assumed:
+The *send()* method may also be invoked without a status code. When invoked in this fashion, a status code of 200 is assumed.
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *message* | String, array or table | Yes | The response’s body. Arrays and tables are automatically JSON-encoded before being sent |
+
+#### Return Value ####
+
+Boolean — `false` if the context has already been used to respond to the request, otherwise `true`.
+
+#### Example #####
```squirrel
app.get("/", function(context) {
@@ -411,11 +711,29 @@ app.get("/", function(context) {
isComplete()
-The *isComplete()* method returns whether or not a response has been sent for the current context. Rocky keeps track of whether or not a response has been sent, and middlewares and route handlers don’t execute if the context has already sent a response. This method should primarily be used for developers extending Rocky.
+This method indicates whether or not a response has been sent for the current context. Rocky keeps track of whether or not a response has been sent, and middlewares and route handlers don’t execute if the context has already sent a response.
+
+This method should primarily be used for developers extending Rocky.
+
+#### Return Value ####
+
+Boolean — `true` if the context’s response has already been sent, otherwise `false`.
+
+
+
+This method attempts to retrieve a header from the context’s [HTTP Request table](https://developer.electricimp.com/api/httprequest).
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *name* | String | Yes | The header’s name |
+
+#### Return Value ####
-
+String — If the header is present, the value of the header; otherwise `null`.
-The *getHeader()* method attempts to retrieve a header from the HTTP Request table. If the header is present, the value of that header is returned, if the header is not present `null` will be returned.
+#### Example ####
```squirrel
// user:password
@@ -428,23 +746,138 @@ app.get("/", function(context) {
});
```
-
+
-The *setHeader()* method adds the specified header to the HTTPResponse object sent during [context.send](#context_send). In the following example, we create a new user resource and return the location of that resource with a `location` header:
+This method adds the specified header to the [HTTPResponse](https://developer.electricimp.com/api/httpresponse) object sent by calling [*send()*](#context_send).
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *name* | String | Yes | The header’s name |
+| *value* | String | Yes | The header’s value |
+
+#### Return Value ####
+
+Nothing.
+
+#### Example ####
```squirrel
app.get("/", function(context) {
// Redirect requests made to / to /index.html
+ // Add a `location` header
context.setHeader("Location", http.agenturl() + "/index.html");
context.send(301);
});
```
+setTimeout(timeout[, callback][, exceptionHandler])
+
+This method allows you to specify a timeout for the context. Calling this method immediately sets a timer which will fire when the timeout is exceeded. This sets a time limit before which the context must be resolved by calling [*send()*](#context_send).
+
+If the timer fires and no function has been passed into *callback*, then the context will be sent with a status code of 504 (gateway timeout).
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *timeout* | Float or integer | Yes | The new timeout setting |
+| *callback* | Function | No | A handler to be called if the timeout is exceeded |
+| *exceptionHandler* | Function | No | A handler to be called if the callback triggers a runtime error |
+
+#### Returns ####
+
+Nothing.
+
+isBrowser()
+
+This method indicates whether the `Accept: text/html` header was present.
+
+#### Return Value ####
+
+Boolean — `true` if the `Accept: text/html` header was present; otherwise `false`.
+
+#### Example ####
+
+```squirrel
+const INDEX_HTML = @"
+
+
+ My Agent
+
+
+ Hello World!
+
+
+";
+
+app.get("/", function(context) {
+ context.send(200, { message = "Hello World!" });
+});
+
+app.get("/index.html", function(context) {
+ if (!context.isBrowser()) {
+ // If it was an API request
+ context.setHeader("location", http.agenturl());
+ context.send(301);
+ return;
+ }
+
+ // If it was a browser request:
+ context.send(200, INDEX_HTML);
+});
+```
+
+## Rocky.Context Class Methods ##
+
+Rocky.Context.get(id)
+
+This method allows you to retrieve a specific context as referenced by its unique ID.
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *id* | String | Yes | The ID of the required context |
+
+#### Returns ####
+
+[Rocky.Context](#context) — the requested context object, or `null` if the ID is unrecognized.
+
+Rocky.Context.sendToAll(statuscode, response[, headers])
+
+This method sends a response to **all** open requests. The preferred way of invoking this method is by calling Rocky’s [*sendToAll()*](#rocky_sendtoall) method.
+
+#### Parameters ####
+
+| Parameter | Type | Required? | Description |
+| --- | --- | --- | --- |
+| *statuscode* | Integer | Yes | The response’s [HTTP status code](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes) |
+| *response* | String | Yes | The response’s body |
+| *headers* | Table | No | Additional response headers and their values. Default: no extra headers |
+
+#### Returns ####
+
+Nothing.
+
+## Rocky.Context Properties ##
+
context.req
-The *context.req* property is a representation of the HTTP Request table. All fields available in the [HTTP Request Table](http://electricimp.com/docs/api/http/onrequest) can be accessed through this property.
+The *req* property is a representation of the underlying [HTTP Request table](https://developer.electricimp.com/api/httprequest). All fields available in the HTTP Request table can be accessed through this property.
+
+If a `content-type` header was included in the request, and the content type was set to `application/json` or `application/x-www-form-urlencoded`, the *body* property of the request will be a table representing the parsed data, rather than the raw body.
+
+If the content type was set to `multipart/form-data;`, the *body* property will be an array of tables.
+
+**Note 1** If the application requires access to the raw and *unparsed* body of the request, this can be accessed at *context.req.rawbody*.
+
+**Note 2** If you make the *http.post()* call without any HTTP headers explicitly specified, you may end up receiving a request with the `application/x-www-form-urlencoded` content type.
+
+#### Examples ####
-If a `content-type` header was included in the request, and the content type was set to `application/json` or `application/x-www-form-urlencoded`, the *body* property of the request will be a table representing the parsed data, rather than the raw body. If the content type was set to `multipart/form-data;`, the *body* property will be an array of tables. In the following example, we assume requests made to POST */users* include a `content-type` header:
+In the following example, we assume requests made to POST */users* include a `content-type` header:
```squirrel
app.post("/users", function(context) {
@@ -478,11 +911,7 @@ app.post("/users", function(context) {
});
```
-**Note** If the application requires access to the raw and *unparsed* body of the request, it can be accessed with *context.req.rawbody*.
-
-**Note** If you make the *http.post()* call without any HTTP headers explicitly specified, you may end up receiving a request with the `application/x-www-form-urlencoded` content type.
-
-To see the difference between *context.req.body* and *context.req.rawbody*, please take a look at following samples. First, code to send post request:
+The following examples show the difference between *context.req.body* and *context.req.rawbody*. First, code to send a post request:
```squirrel
// Note that application/x-www-form-urlencoded content-type is added to headers by default
@@ -492,10 +921,9 @@ local req = http.post( (http.agenturl() + "/data"), {}, "hello world" )
})
```
-A way to get parsed request body as a table:
+Now, a way to get the parsed request body as a table:
```squirrel
-
app.post("/data", function(context) {
// In this case table identifier will be printed in the server log
server.log(context.req.body);
@@ -503,10 +931,9 @@ app.post("/data", function(context) {
});
```
-And a way to get unparsed request body as a string:
+And a way to get the unparsed request body as a string:
```squirrel
-
app.post("/data", function(context) {
// In this case string "hello world" will be printed in the server log
server.log(context.req.rawbody);
@@ -516,29 +943,14 @@ app.post("/data", function(context) {
context.id
-The *id* property is a unique ID that identifies the context. This is primarily used during long-running tasks and asynchronous requests. See [rocky.getContext](#rocky_getcontext) for example usage.
-
-context.userdata
-
-The *userdata* property can be used by the developer to store any information relevant to the current context. This is primarily used during long-running tasks and asynchronous requests.
-
-```squirrel
-app.get("/temp", function(context) {
- context.userdata = { "startTime": time() };
- device.send("getTemp", context.id);
-});
-
-device.on("getTempResponse", function(data) {
- local context = app.getContext(data.id);
- local roundTripTime = time() - context.userdata.startTime;
- context.send(200, { "temp": data.temp, "requestTime": roundTripTime });
-});
-```
+The *id* property is a unique value that identifies the context. It is primarily used during long-running tasks and asynchronous requests. See Rocky’s [*getContext()*](#rocky_getcontext) method for an example of its usage.
context.path
The *path* property is an array that contains each element in the path. If a request is made to `/a/b/c` then *path* will be `["a", "b", "c"]`.
+#### Example ####
+
```squirrel
app.get("/users/([^/]*)", function(context) {
// Grab the username from the path
@@ -557,7 +969,9 @@ app.get("/users/([^/]*)", function(context) {
context.matches
-The *matches* property is an array that represents the results of the regular expression used to find a matching route. If you included a regular expression in your signature, you can use the matches array to access any expressions you may have captured. The first element of the *matches* array will always be the full path.
+The *matches* property is an array that represents the results of the regular expression used to find a matching route. If you included a regular expression in your [signature](#signatures), you can use the *matches* array to access any expressions you may have captured. The first element of the array will always be the full path.
+
+#### Example ####
```squirrel
app.get("/users/([^/]*)", function(context) {
@@ -575,54 +989,56 @@ app.get("/users/([^/]*)", function(context) {
});
```
-context.isbrowser()
+context.userdata
-The *isbrowser()* method returns true if an `Accept: text/html` header was present.
+The *userdata* property can be used by the developer to store any information relevant to the current context. This is primarily used during long-running tasks and asynchronous requests.
-**Note** The *isbrowser()* method is all lowercase (as opposed to lowerCamelCase).
+#### Example ####
```squirrel
-const INDEX_HTML = @"
-
-
- My Agent
-
-
- Hello World!
-
-
-";
-
-app.get("/", function(context) {
- context.send(200, { message = "Hello World!" });
+app.get("/temp", function(context) {
+ context.userdata = { "startTime": time() };
+ device.send("getTemp", context.id);
});
-app.get("/index.html", function(context) {
- if (!context.isbrowser()) {
- // If it was an API request
- context.setHeader("location", http.agenturl());
- context.send(301);
- return;
- }
-
- // If it was a browser request:
- context.send(200, INDEX_HTML);
+device.on("getTempResponse", function(data) {
+ local context = app.getContext(data.id);
+ local roundTripTime = time() - context.userdata.startTime;
+ context.send(200, { "temp": data.temp, "requestTime": roundTripTime });
});
```
-Rocky.Context.sendToAll(statuscode, response[, headers])
+context.sent
-The static *sendToAll()* method sends a response to **all** open requests. The preferred way of invoking this method is through [*Rocky.sendToAll()*](#rocky_sendtoall).
+The *sent* property is **deprecated**. Developers should instead call [*isComplete()*](#context_iscomplete).
-context.sent
+Signatures
-The *sent* property is **deprecated**. Developers should move to using the [*isComplete()*](#context_iscomplete) method instead.
+Signatures can either be fully qualified paths (`/led/state`) or include regular expressions (`/users/([^/]*)`). If the path is specified using a regular expression, any matches will be added to the [Rocky.Context](#context) object passed into the callback.
+
+In the following example, we capture the desired user’s username:
+
+```squirrel
+app.get("/users/([^/]*)", function(context) {
+ // Grab the username from the regex
+ // (context.matches[0] will always be the full path)
+ local username = context.matches[1];
+
+ if (username in usersTable) {
+ // If we found the user, return the user object
+ context.send(usersTable[username]);
+ } else {
+ // If the user doesn't exist, return a 404
+ context.send(404, { "error": "Unknown User" });
+ }
+});
+```
Middleware
-Middleware allows you to easily (and scalably) add new functionality to your request handlers. Middleware functions can be attached at either a global level through [*Rocky.use()*](#rocky_use), or at the route level with [*Rocky.Route.use()*](#route_use). Middleware functions are invoked before the main request handler and can aid in debugging, data validation/transformation and more.
+Middleware allows you to add new functionality to your request handlers easily and scalably. Middleware functions can be attached at either a global level through Rocky’s [*use()*](#rocky_use) method, or at the route level with [*Rocky.Route.use()*](#route_use). Middleware functions are invoked before the main request handler and can aid in debugging, data validation and transformation, and more.
-Middleware functions are invoked with two parameters: a [Rocky.Context](#context) object and a *next* function. The *next* function invokes the next middleware/handler in the chain (see [Order of Execution](middleware_orderofexecution)).
+Middleware functions are invoked with two parameters: a [Rocky.Context](#context) object and a reference, *next*, to the next middleware/handler in the chain (see [**Order of Execution**](#middleware_orderofexecution), below). At the end of the middleware, always call this reference as a function to ensure the next middleware is executed. If there is no subsequent middleware, the call to *next* hands control back to Rocky.
Responding to a request in a middleware prevents further middleware functions and event handlers (such as *authorize*, *onAuthorized*, etc) from executing.
@@ -636,11 +1052,11 @@ function debuggingMiddleware(context, next) {
server.log(" PATH: " + context.req.path.tolower());
server.log(" TIME: " + time());
- // Invoke the next middleware
+ // Invoke the next middleware in the sequence
next();
}
-app <- Rocky();
+app <- Rocky.init();
app.use(debuggingMiddleware);
app.get("/", function(context) {
@@ -687,7 +1103,7 @@ function validateDataMiddleware(context, next) {
next();
}
-app <- Rocky();
+app <- Rocky.init();
// Requests to GET /data will execute readAuthMiddleware,
// then the route handler if the readAuthMiddle didn't respond
@@ -709,7 +1125,7 @@ app.post("/data", function(context) {
}).use([writeAuthMiddleware, validateDataMiddleware]);
```
-The *next* method allows you to complete asynchronous operations before moving on to the next middleware or handler. In the following example, we lookup a userId from a remote service before moving on:
+Having access to the function referenced by *next* allows you to complete asynchronous operations before moving on to the next middleware or handler. In the following example, we look up a user ID from a remote service before moving on:
```squirrel
function userIdMiddleware(context, next) {
@@ -740,18 +1156,19 @@ app.get("/user", function(context) {
Order of Execution
-When Rocky processes an incoming HTTPS request, the following takes place:
+When Rocky processes an incoming HTTPS request, the following sequence of events takes place:
-- Rocky adds the access control headers unless the `accessControl` setting is set to `false`
-- Rocky rejects non-HTTPS requests unless the `allowUnsecure` setting is not set to `true`
-- Rocky parses the body (and send a 400 response if there was an error parsing the data)
-- Invoke the Rocky-level middleware functions
-- Invoke the Route-level middleware functions
-- Invoke the authorize function, and based on the return on authorize:
- - Invokes the request handler (*isAuthorized* returned `true`)
- - Invokes the onUnauthorized handler (*isAuthorized* returned `false`)
+- Rocky adds the access control headers unless the `accessControl` setting (see [*rocky.init()*](#rocky_init)) is set to `false`.
+- Rocky rejects non-HTTPS requests unless the `allowUnsecure` setting (see [*rocky.init()*](#rocky_init)) is set to `true`.
+- Rocky parses the request body.
+ - Rocky sends a 400 response if there was an error parsing the data.
+- Global-level middleware functions are invoked.
+- Route-level middleware functions are invoked.
+- If present, the global authorization function is invoked.
+ - If the global authorization function returned `true`, the global request handler is invoked.
+ - If the global authorization function returned `false`, the [global unauthorized handler](#rocky_onunauthorized) is invoked.
-If a middleware function send a response, no further action will be taken on the request.
+If a middleware function sends a response, no further action will be taken on the request.
If a runtime errors occurs after the data has been parsed, the *onError* handler will be invoked.
@@ -759,7 +1176,7 @@ If a runtime errors occurs after the data has been parsed, the *onError* handler
During a cross domain AJAX request, some browsers will send a [preflight request](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing#Preflight_example) to determine if it has the permissions needed to perform the action.
-To accomodate preflight requests you can add a wildcard OPTIONS handler:
+To accommodate preflight requests you can add a wildcard `OPTIONS` handler:
```squirrel
app.on("OPTIONS", ".*", function(context) {
@@ -775,7 +1192,7 @@ Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept
Access-Control-Allow-Methods: POST, PUT, GET, OPTIONS
```
-If you wish to override the default headers, you can instantiate Rocky with the `accessControl` setting set to `false`, and use a middleware to add the headers you wish to include:
+If you wish to override these default headers, you can instantiate Rocky with the `accessControl` setting set to `false`, and use a middleware to add the headers you wish to include. For example:
```squirrel
function customCORSMiddleware(context, next) {
@@ -787,10 +1204,10 @@ function customCORSMiddleware(context, next) {
next();
}
-app <- Rocky( { "accessControl": false });
+app <- Rocky.init({ "accessControl": false });
app.use([ customCORSMiddleware ]);
```
-## License
+## License ##
-Rocky is licensed under [MIT License](https://github.com/electricimp/Rocky/blob/master/LICENSE).
+This library is licensed under [MIT License](./LICENSE).
diff --git a/RGBLed-Example/rgb.agent.nut b/RGBLed-Example/rgb.agent.nut
index 547575c..eda4efc 100644
--- a/RGBLed-Example/rgb.agent.nut
+++ b/RGBLed-Example/rgb.agent.nut
@@ -1,8 +1,8 @@
-// Copyright (c) 2015 Electric Imp
+// Copyright (c) 2015-19 Electric Imp
// This file is licensed under the MIT License
// http://opensource.org/licenses/MIT
-#require "Rocky.class.nut:2.0.0"
+#require "Rocky.agent.lib.nut:3.0.0"
/******************** Application Code ********************/
led <- {
@@ -14,7 +14,7 @@ device.on("info", function(data) {
led = data;
});
-app <- Rocky();
+app <- Rocky.init();
app.get("/color", function(context) {
context.send(200, { color = led.color });
@@ -32,7 +32,8 @@ app.post("/color", function(context) {
// if preflight check passed - do things
led.color = context.req.body.color;
- device.send("setColor", context.req.body.color);
+ server.log("Setting color to R: " + led.color.red + ", G: " + led.color.green + ", B: " + led.color.blue);
+ device.send("setColor", led.color);
// send the response
context.send({ "verb": "POST", "led": led });
@@ -53,7 +54,8 @@ app.post("/state", function(context) {
// if preflight check passed - do things
led.state = context.req.body.state;
- device.send("setState", context.req.body.state);
+ server.log("Setting state to " + (led.state ? "on" : "off"));
+ device.send("setState", led.state);
// send the response
context.send({ "verb": "POST", "led": led });
diff --git a/RGBLed-Example/rgb.device.nut b/RGBLed-Example/rgb.device.nut
index c36b571..9e46292 100644
--- a/RGBLed-Example/rgb.device.nut
+++ b/RGBLed-Example/rgb.device.nut
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Electric Imp
+// Copyright (c) 2015-19 Electric Imp
// This file is licensed under the MIT License
// http://opensource.org/licenses/MIT
diff --git a/Rocky.agent.lib.nut b/Rocky.agent.lib.nut
new file mode 100644
index 0000000..f279ce1
--- /dev/null
+++ b/Rocky.agent.lib.nut
@@ -0,0 +1,1103 @@
+enum ROCKY_ERROR {
+ PARSE = "Error parsing body of request",
+ BAD_MIDDLEWARE = "Invalid middleware -- middleware must be a function",
+ TIMEOUT = "Bad timeout value - must be an integer or float",
+ NO_BOUNDARY = "No boundary found in content-type",
+ BAD_CALLBACK = "Invald callback -- callback must be a function"
+}
+
+/**
+ * This class allows you to define and operate an agent-served API.
+ *
+ * @copyright Electric Imp, Inc. 2015-19
+ * @license MIT
+ *
+ * @table
+ *
+*/
+Rocky <- {
+
+ "VERSION": "3.0.0",
+
+ // ------------------ PRIVATE PROPERTIES ------------------//
+
+ // Route handlers, event handers and middleware
+ // are all stored in the same table
+ "_handlers": null,
+
+ // Settings
+ "_timeout": 10,
+ "_strictRouting": false,
+ "_allowUnsecure": false,
+ "_accessControl": true,
+ "_sigCaseSensitive": false,
+
+ /**
+ * The Rocky initializer. In 3.0.0, this replaces the class constructor.
+ *
+ * @constructor
+ *
+ * @param {table} settings - Optional instance behavior settings. Default: see values above.
+ *
+ * @returns {table} Rocky.
+ *
+ */
+ "init": function(settings = {}) {
+ // Set defaults on a re-call
+ _setDefaults();
+
+ // Initialize settings, checking values as appropriate
+ if ("timeout" in settings && typeof settings.timeout == "bool") _timeout = settings.timeout;
+ if ("allowUnsecure" in settings && typeof settings.allowUnsecure == "bool") _allowUnsecure = settings.allowUnsecure;
+ if ("strictRouting" in settings && typeof settings.strictRouting == "bool") _strictRouting = settings.strictRouting;
+ if ("accessControl" in settings && typeof settings.accessControl == "bool") _accessControl = settings.accessControl;
+ if ("sigCaseSensitive" in settings && typeof settings.sigCaseSensitive == "bool") _sigCaseSensitive = settings.sigCaseSensitive;
+
+ // Inititalize handlers and middleware
+ _handlers = {
+ authorize = _defaultAuthorizeHandler.bindenv(this),
+ onUnauthorized = _defaultUnauthorizedHandler.bindenv(this),
+ onTimeout = _defaultTimeoutHandler.bindenv(this),
+ onNotFound = _defaultNotFoundHandler.bindenv(this),
+ onException = _defaultExceptionHandler.bindenv(this),
+ middlewares = []
+ };
+
+ // Bind the instance's onrequest handler
+ http.onrequest(Rocky._onrequest.bindenv(this));
+
+ return this;
+ },
+
+ //-------------------- STATIC METHODS --------------------//
+
+ /**
+ * Get the specified Rocky context.
+ *
+ * @param {integer} id - The identifier of the desired context.
+ *
+ * @returns {object} The requested Rocky.Context instance.
+ */
+ "getContext": function(id) {
+ return Rocky.Context.get(id);
+ },
+
+ /**
+ * Send a response to to all currently active requests.
+ *
+ * @param {integer} statuscode - The response's HTTP status code.
+ * @param {any} response - The response body.
+ * @param {table} headers - Optional additional response headers. Default: no additional headers.
+ *
+ */
+ "sendToAll": function(statuscode, response, headers = {}) {
+ Rocky.Context.sendToAll(statuscode, response, headers);
+ },
+
+ //-------------------- PUBLIC METHODS --------------------//
+
+ // ------------------- REQUESTS --------------------//
+
+ /**
+ * Register a handler for a non-standard (GET, PUT or POST) HTTP request.
+ *
+ * @param {string} verb - The HTTP request verb.
+ * @param {string} signature - An endpoint path signature.
+ * @param {function} callback - The handler that will process this kind of request.
+ * @param {integer} timeout - Optional timeout period in seconds. Default: the class-level value.
+ *
+ * @returns {object} A Rocky.Route instance for the handler.
+ */
+ "on": function(verb, signature, callback, timeout = null) {
+ // Check timeout and set it to class-level timeout if not specified for route
+ if (timeout == null) timeout = this._timeout;
+
+ // Register this verb and signature against the callback
+ verb = verb.toupper();
+ // ADDED 3.0.0 -- Manage signature case (see https://github.com/electricimp/Rocky/issues/36)
+ if (!_sigCaseSensitive) signature = signature.tolower();
+ if (!(signature in _handlers)) _handlers[signature] <- {};
+
+ local routeHandler = Rocky.Route(callback);
+ routeHandler.setTimeout(timeout);
+ _handlers[signature][verb] <- routeHandler;
+ return routeHandler;
+ },
+
+ /**
+ * Register a handler for an HTTP POST request.
+ *
+ * @param {string} signature - An endpoint path signature.
+ * @param {function} callback - The handler that will process the POST request.
+ * @param {integer} timeout - Optional timeout in seconds. Default: the class-level value.
+ *
+ * @returns {object} A Rocky.Route instance for the handler.
+ */
+ "post": function(signature, callback, timeout = null) {
+ return on("POST", signature, callback, timeout);
+ },
+
+ /**
+ * Register a handler for an HTTP GET request.
+ *
+ * @param {string} signature - An endpoint path signature.
+ * @param {function} callback - The handler that will process the GET request.
+ * @param {integer} timeout - Optional timeout in seconds. Default: the class-level value.
+ *
+ * @returns {object} A Rocky.Route instance for the handler.
+ */
+ "get": function(signature, callback, timeout = null) {
+ return on("GET", signature, callback, timeout);
+ },
+
+ /**
+ * Register a handler for an HTTP PUT request.
+ *
+ * @param {string} signature - An endpoint path signature.
+ * @param {function} callback - The handler that will process the PUT request.
+ * @param {integer} timeout - Optional timeout in seconds. Default: the class-level value.
+ *
+ * @returns {object} A Rocky.Route instance for the handler.
+ */
+ "put": function(signature, callback, timeout = null) {
+ return on("PUT", signature, callback, timeout);
+ },
+
+ // ------------------- AUTHORIZATION -------------------//
+
+ /**
+ * Register a handler for request authorization.
+ *
+ * @param {Function} callback - The handler that will process authorization requests.
+ *
+ * @returns {object} The Rocky instance (this).
+ */
+ "authorize": function(callback) {
+ _handlers.authorize <- callback;
+ return this;
+ },
+
+ /**
+ * Register a handler for processing rejected requests.
+ *
+ * @param {function} callback - The handler that will process rejected requests.
+ *
+ * @returns {object} The Rocky instance (this).
+ */
+ "onUnauthorized": function(callback) {
+ _handlers.onUnauthorized <- callback;
+ return this;
+ },
+
+ // ------------------- EVENTS -------------------//
+
+ /**
+ * Register a handler for timed out requests.
+ *
+ * @param {function} callback - The handler that will process request time-outs.
+ * @param {integer/float} timeout - Optional timeout in seconds. Default: the class-level value.
+ *
+ * @returns {object} The Rocky instance (this).
+ */
+ "onTimeout": function(callback, timeout = null) {
+ if (timeout != null) _timeout = timeout;
+ _handlers.onTimeout <- callback;
+ return this;
+ },
+
+ /**
+ * Register a handler for requests asking for missing resources.
+ *
+ * @param {function} callback - The handler that will process 'resource not found' requests.
+ *
+ * @returns {object} The Rocky instance (this).
+ */
+ "onNotFound": function(callback) {
+ _handlers.onNotFound <- callback;
+ return this;
+ },
+
+ /**
+ * Register a handler for requests that triggered an exception.
+ *
+ * @param {function} callback - The handler that will process the failed request.
+ *
+ * @returns {object} The Rocky instance (this).
+ */
+ "onException": function(callback) {
+ _handlers.onException <- callback;
+ return this;
+ },
+
+ // ------------------- MIDDLEWARES -------------------//
+
+ /**
+ * Register one or more user-defined request-processing middlewares.
+ *
+ * @param {function/array} middlewares - One or more middleware function references.
+ *
+ * @returns {object} The Rocky instance (this).
+ */
+ "use": function(middlewares) {
+ if (typeof middlewares == "function") {
+ _handlers.middlewares.push(middlewares);
+ } else if (typeof _handlers.middlewares == "array") {
+ foreach (middleware in middlewares) use(middleware);
+ } else {
+ throw ROCKY_ERROR.BAD_MIDDLEWARE;
+ }
+
+ return this;
+ },
+
+ //-------------------- PRIVATE METHODS --------------------//
+
+ /**
+ * Apply default headers to the specified reponse object.
+ *
+ * @param {object} res - An imp API HTTPResponse instance.
+ *
+ * @private
+ */
+ "_addAccessControl": function(res) {
+ res.header("Access-Control-Allow-Origin", "*")
+ res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
+ res.header("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS");
+ },
+
+ /**
+ * The core Rocky incoming HTTP request handler.
+ *
+ * @param {object} req - The source imp API HTTPRequest object.
+ * @param {object} res - An imp API HTTPResponse object primed to respond to the request.
+ *
+ * @private
+ */
+ "_onrequest": function(req, res) {
+ // Add access control headers if required
+ if (_accessControl) _addAccessControl(res);
+
+ // Setup the context for the callbacks
+ local context = Rocky.Context(req, res);
+
+ // Check for unsecure reqeusts
+ if (_allowUnsecure == false && "x-forwarded-proto" in req.headers && req.headers["x-forwarded-proto"] != "https") {
+ context.send(405, "HTTP not allowed.");
+ return;
+ }
+
+ // Parse the request body back into the body
+ try {
+ req.rawbody <- req.body;
+ req.body = _parse_body(req);
+ } catch (err) {
+ context.send(400, ROCKY_ERROR.PARSE);
+ return;
+ }
+
+ // Look for a handler for this path
+ local route = _handler_match(req);
+ if (route) {
+ // if we have a handler
+ context.path = route.path;
+ context.matches = route.matches;
+
+ // parse auth
+ context.auth = _parse_authorization(context);
+
+ // Create timeout
+ local onTimeout = _handlers.onTimeout;
+ local timeout = route.handler.getTimeout();
+
+ if (route.handler.hasHandler("onTimeout")) onTimeout = route.handler.getHandler("onTimeout");
+
+ context.setTimeout(timeout, onTimeout, _handlers.onException);
+ route.handler.execute(context, _handlers);
+ } else {
+ // If we don't have a handler
+ // FROM 3.0.0 -- manage exceptions thrown in the handler
+ try {
+ _handlers.onNotFound(context);
+ } catch (ex) {
+ _handlers.onException(context, ex);
+ }
+ }
+ },
+
+ /**
+ * Parse an HTTP request's body based on the request's content type.
+ *
+ * @param {object} req - The source imp API HTTPRequest object.
+ *
+ * @returns {string} The parsed request body.
+ *
+ * @private
+ */
+ "_parse_body": function (req) {
+ local contentType = "content-type" in req.headers ? req.headers["content-type"] : "";
+
+ if (contentType == "application/json" || contentType.find("application/json;") != null) {
+ if (req.body == "" || req.body == null) return null;
+ return http.jsondecode(req.body);
+ }
+
+ if (contentType == "application/x-www-form-urlencoded" || contentType.find("application/x-www-form-urlencoded;") != null) {
+ if (req.body == "" || req.body == null) return null;
+ return http.urldecode(req.body);
+ }
+
+ if (contentType.find("multipart/form-data;") == 0) {
+ local parts = [];
+
+ // Find the boundary in the contentType
+ local boundary;
+ local findString = regexp(@"boundary([ ]*)=");
+ local match = findString.search(contentType);
+
+ if (match) {
+ boundary = contentType.slice(match.end);
+ boundary = strip(boundary);
+ } else {
+ throw ROCKY_ERROR.NO_BOUNDARY;
+ }
+
+ // Remove all carriage returns from string (to support either \r\n or \n for linebreaks)
+ local body = "";
+ local bodyLines = split(req.body, "\r");
+ foreach (i, line in bodyLines) body += line;
+
+ // Find all boundaries in the body
+ local boundaries = [];
+ local bregex = regexp2(@"--" + boundary);
+ local bmatch = bregex.search(body);
+ while (bmatch != null) {
+ boundaries.push(bmatch);
+ bmatch = bregex.search(body, bmatch.begin+1);
+ }
+
+ // Create array of parts
+ for (local i = 0; i < boundaries.len() - 1; i++) {
+ // Extract one part from the request
+ local part = body.slice(boundaries[i].end + 1, boundaries[i+1].begin);
+
+ // Split the part into headers and body
+ local partSplit = regexp2("\n\n").search(part);
+ local header = part.slice(0, partSplit.begin);
+ local data = part.slice(partSplit.end, -1);
+
+ // Create table to store the parsed content of the part
+ local parsedPart = {};
+ parsedPart.name <- null;
+ parsedPart.data <- data;
+
+ // Extract each individual header and store in parsedPart
+ local keyValueRegex = regexp2(@"(^|\W)(\S+)\s*[=:]\s*(""[^""]*""|\S*)");
+ local keyValueCapture = keyValueRegex.capture(header);
+ while (keyValueCapture != null) {
+ // Extract key and value for the header
+ local key = header.slice(keyValueCapture[2].begin, keyValueCapture[2].end);
+ local val = header.slice(keyValueCapture[3].begin, keyValueCapture[3].end);
+
+ // Remove any quotations
+ if (val[0] == '"') val = val.slice(1, -1);
+
+ // Save the header in parsedPart
+ parsedPart[key] <- val;
+ keyValueCapture = keyValueRegex.capture(header, keyValueCapture[0].end);
+ }
+
+ // Add the parsed part to the array of parts
+ parts.push(parsedPart);
+ }
+
+ // Return the array of parts
+ return parts;
+ }
+
+ // Nothing matched, send back the original body
+ return req.body;
+ },
+
+ /**
+ * Parse an HTTP request's authorization credentials.
+ *
+ * @param {object} context - The Rocky.Context containing the HTTPRequest.
+ *
+ * @returns {table} The parsed authorization credentials.
+ *
+ * @private
+ */
+ "_parse_authorization": function(context) {
+ if ("authorization" in context.req.headers) {
+ local auth = split(context.req.headers.authorization, " ");
+
+ if (auth.len() == 2 && auth[0] == "Basic") {
+ // Note the username and password can't have colons in them
+ local creds = http.base64decode(auth[1]).tostring();
+ creds = split(creds, ":");
+ if (creds.len() == 2) {
+ return { authType = "Basic", user = creds[0], pass = creds[1] };
+ }
+ } else if (auth.len() == 2 && auth[0] == "Bearer") {
+ // The bearer is just the password
+ if (auth[1].len() > 0) {
+ return { authType = "Bearer", user = auth[1], pass = auth[1] };
+ }
+ }
+ }
+
+ return { authType = "None", user = "", pass = "" };
+ },
+
+ /**
+ * Separate out the components of a request's target path.
+ *
+ * @param {object} routeHandler - The Rocky.Route instance describing an endpoint.
+ * @param {string} path - The endpoint path.
+ * @param {object} regexp - An imp API Regexp instance. Default: null.
+ *
+ * @returns {table} The parsed path components.
+ *
+ * @private
+ */
+ "_extract_parts": function(routeHandler, path, regexp = null) {
+ // Set up the table we will return
+ local parts = { path = [], matches = [], handler = routeHandler };
+
+ // Split the path into parts
+ foreach (part in split(path, "/")) parts.path.push(part);
+
+ // Capture regular expression matches
+ if (regexp != null) {
+ local caps = regexp.capture(path);
+ local matches = [];
+ foreach (cap in caps) {
+ parts.matches.push(path.slice(cap.begin, cap.end));
+ }
+ }
+
+ return parts;
+ },
+
+ /**
+ * Process regular expression matches against an endpoint path.
+ *
+ * @param {object} req - The source imp API HTTPRequest object.
+ *
+ * @returns {table} The processed components, or null.
+ *
+ * @private
+ */
+ "_handler_match": function(req) {
+ // ADDED 3.0.0 -- manage signature case (see https://github.com/electricimp/Rocky/issues/36)
+ local signature = req.path;
+ if (!_sigCaseSensitive) signature = signature.tolower();
+ local verb = req.method.toupper();
+
+ // ignore trailing /s if _strictRouting == false
+ if (!_strictRouting) {
+ while (signature.len() > 1 && signature[signature.len() - 1] == '/') {
+ signature = signature.slice(0, signature.len() - 1);
+ }
+ }
+
+ if ((signature in _handlers) && (verb in _handlers[signature])) {
+ // We have an exact signature match
+ return _extract_parts(_handlers[signature][verb], signature);
+ } else if ((signature in _handlers) && ("*" in _handlers[signature])) {
+ // We have a partial signature match
+ return _extract_parts(_handlers[signature]["*"], signature);
+ } else {
+ // Let's iterate through all handlers and search for a regular expression match
+ foreach (_signature,_handler in _handlers) {
+ if (typeof _handler == "table") {
+ foreach (_verb,_callback in _handler) {
+ if (_verb == verb || _verb == "*") {
+ try {
+ local ex = regexp(_signature);
+ if (ex.match(signature)) {
+ // We have a regexp handler match
+ return _extract_parts(_callback, signature, ex);
+ }
+ } catch (e) {
+ // Don't care about invalid regexp.
+ }
+ }
+ }
+ }
+ }
+ }
+ return null;
+ },
+
+ //-------------------- DEFAULT HANDLERS --------------------//
+
+ /**
+ * Process authorization requests: just accept the request.
+ *
+ * @param {object} context - The Rocky.Context containing the request.
+ *
+ * @returns {Bool} Always authorized (true)
+ *
+ * @private
+ */
+ "_defaultAuthorizeHandler": function(context) {
+ return true;
+ },
+
+ /**
+ * Process rejected requests: issue a 401 response.
+ *
+ * @param {object} context - The Rocky.Context containing the request.
+ *
+ * @private
+ */
+ "_defaultUnauthorizedHandler": function(context) {
+ context.send(401, "Unauthorized");
+ },
+
+ /**
+ * Process requests to missing resources: issue a 404 response.
+ *
+ * @param {object} context - The Rocky.Context containing the request.
+ *
+ * @private
+ */
+ "_defaultNotFoundHandler": function(context) {
+ context.send(404, format("No handler for %s %s", context.req.method, context.req.path));
+ },
+
+ /**
+ * Process timed out requests: issue a 500 response.
+ *
+ * @param {object} context - The Rocky.Context containing the request.
+ *
+ * @private
+ */
+ "_defaultTimeoutHandler": function(context) {
+ context.send(500, format("Agent Request timed out after %i seconds.", _timeout));
+ },
+
+ /**
+ * Process requests that trigger exceptions: issue a 500 response.
+ *
+ * @param {object} context - The Rocky.Context containing the request.
+ * @param {String} ex - The triggered exception/error message.
+ *
+ * @private
+ */
+ "_defaultExceptionHandler": function(context, ex) {
+ server.error(ex);
+ context.send(500, "Agent Error: " + ex);
+ },
+
+ /**
+ * Set Rocky defaults.
+ *
+ * @private
+ */
+ "_setDefaults": function() {
+ _timeout = 10;
+ _strictRouting = false;
+ _allowUnsecure = false;
+ _accessControl = true;
+ _sigCaseSensitive = false;
+ }
+}
+
+
+/**
+ * This class defines a handler for an event, eg. request authorization, time out or
+ * a triggered exception, or some other, user-defined action (ie. a 'middleware').
+ *
+ * @copyright Electric Imp, Inc. 2015-19
+ * @license MIT
+ *
+ * @class
+ *
+*/
+class Rocky.Route {
+
+ //------------------ PRIVATE PROPERTIES ------------------//
+ _handlers = null;
+ _timeout = null;
+ _callback = null;
+
+ /**
+ * The Rocky.Route constructor. Not called by user code, only by Rocky instances.
+ *
+ * @constructor
+ *
+ * @param {function} callback - The endpoint handler.
+ *
+ * returns {object} The Rocky.Route instance (this).
+ *
+ */
+ constructor(callback) {
+ _handlers = { middlewares = [] };
+ _timeout = 10;
+ _callback = callback;
+ }
+
+ //-------------------- PUBLIC METHODS --------------------//
+
+ /**
+ * Run the registered handlers (or defaults where no handlers are registered).
+ * NOTE This is public because it is used outside of the class (ie. by the Rocky singleton),
+ * but it is not expected to be called directly by application code, so is not
+ * formally documented.
+ *
+ * @param {object} context - The Rocky.Context containing the request.
+ * @param {Array} defaultHandlers - The currently registered handlers.
+ *
+ */
+ function execute(context, defaultHandlers) {
+ // NOTE: Copying these handlers into the route might have some unintended side effect.
+ // Consider changing this if issues come up.
+ foreach (handlerName, handler in defaultHandlers) {
+ // Copy over the non-middleware handlers
+ if (handlerName != "middlewares") {
+ if (!hasHandler(handlerName)) { _setHandler(handlerName, handler); }
+ } else {
+ // Copy the handlers over so we can iterate through in
+ // the correct order:
+ for (local i = handler.len() -1; i >= 0; i--) {
+ // Only add handlers that we haven't already added
+ if (_handlers.middlewares.find(handler[i]) == null) {
+ _handlers.middlewares.insert(0, handler[i]);
+ }
+ }
+ }
+ }
+
+ // Run all the handlers
+ _invokeNextHandler(context);
+ }
+
+ /**
+ * Register a route-level authorization handler.
+ *
+ * @param {function} callback - The handler that will process route-level requests.
+ *
+ * @returns {object} The target Rocky.route instance (this).
+ */
+ function authorize(callback) {
+ if (typeof callback != "function") throw ROCKY_ERROR.BAD_CALLBACK;
+ return _setHandler("authorize", callback);
+ }
+
+ /**
+ * Register a route-level 'request rejected' handler.
+ *
+ * @param {function} callback - The handler that will process route-level requests.
+ *
+ * @returns {object} The target Rocky.route instance (this).
+ */
+ function onUnauthorized(callback) {
+ if (typeof callback != "function") throw ROCKY_ERROR.BAD_CALLBACK;
+ return _setHandler("onUnauthorized", callback);
+ }
+
+ /**
+ * Register a route-level authorization handler.
+ *
+ * @param {function} callback - The handler that will process route-level requests.
+ *
+ * @returns {object} The target Rocky.route instance (this).
+ */
+ function onException(callback) {
+ if (typeof callback != "function") throw ROCKY_ERROR.BAD_CALLBACK;
+ return _setHandler("onException", callback);
+ }
+
+ /**
+ * Register a route-level request timeout handler.
+ *
+ * @param {function} callback - The handler that will process route-level requests.
+ * @param {integer} timeout - The timeout period in seconds.
+ *
+ * @returns {object} The target Rocky.route instance (this).
+ */
+ function onTimeout(callback, timeout = null) {
+ if (typeof callback != "function") throw ROCKY_ERROR.BAD_CALLBACK;
+ if (timeout != null) _timeout = timeout;
+ return _setHandler("onTimeout", callback);
+ }
+
+ /**
+ * Register a route-level middleware.
+ *
+ * @param {function/array} middlewares - One or more references to middlware functions.
+ *
+ * @returns {object} The target Rocky.route instance (this).
+ */
+ function use(middlewares) {
+ if (!hasHandler("middlewares")) { _handlers["middlewares"] <- [] };
+
+ if(typeof middlewares == "function") {
+ _handlers.middlewares.push(middlewares);
+ } else if (typeof _handlers.middlewares == "array") {
+ foreach(middleware in middlewares) use(middleware);
+ } else {
+ throw ROCKY_ERROR.BAD_MIDDLEWARE;
+ }
+
+ return this;
+ }
+
+ /**
+ * Determine if a specified handler has been registered.
+ *
+ * @param {string} handlerName - The name of the handler.
+ *
+ * @returns {Bool} Whether the named handler is registered (true) or not (false).
+ */
+ function hasHandler(handlerName) {
+ return (handlerName in _handlers);
+ }
+
+ /**
+ * Get a specified handler.
+ *
+ * @param {string} handlerName - The name of the handler.
+ *
+ * @returns {function} A reference to the handler.
+ */
+ function getHandler(handlerName) {
+ // Return null if no handler
+ if (!hasHandler(handlerName)) return null;
+
+ // Return the handler if it exists
+ return _handlers[handlerName];
+ }
+
+ /**
+ * Get the route-level request timeout.
+ *
+ * @returns {integer} The timeout period in seconds.
+ */
+ function getTimeout() {
+ return _timeout;
+ }
+
+ /**
+ * Set the route-level request timeout.
+ *
+ * @param {integer} The timeout period in seconds.
+ */
+ function setTimeout(timeout) {
+ return _timeout = timeout;
+ }
+
+ //-------------------- PRIVATE METHODS --------------------//
+
+ /**
+ * Invoke the next middleware, and move the authorize/callback/onUnauthorized
+ * flow on when any registered middlewares have completed.
+ *
+ * @param {object} context - The Rocky.Context instance containing the request.
+ * @param {integer} idx - Index counter for the handler list.
+ *
+ * @private
+ */
+ function _invokeNextHandler(context, idx = 0) {
+ // If we've sent a response, we're done
+ if (context.isComplete()) return;
+
+ // Check if we have middlewares left to execute
+ if (idx < _handlers.middlewares.len()) {
+ try {
+ // if we do, execute them (with a next() function for the next middleware)
+ _handlers.middlewares[idx](context, _nextGenerator(context, idx + 1));
+ } catch (ex) {
+ _handlers.onException(context, ex);
+ }
+ } else {
+ // Otherwise, run the rest of the flow
+ try {
+ // Check if we're authorized
+ if (_handlers.authorize(context)) {
+ // If we're authorized, execute the route handler
+ _callback(context);
+ } else {
+ // if we're unauthorized, execute the onUnauthorized handler
+ _handlers.onUnauthorized(context);
+ }
+ } catch (ex) {
+ _handlers.onException(context, ex);
+ }
+ }
+ }
+
+ /**
+ * Generate 'next()' functions for middlewares. These are supplied to handlers so
+ * that they can invoke the next handler in the sequence.
+ *
+ * @param {object} context - The Rocky.Context instance containing the request.
+ * @param {integer} idx - Index counter for the handler list.
+ *
+ * @returns {function} The next function to excecute in the middlware sequence.
+ *
+ * @private
+ */
+ function _nextGenerator(context, idx) {
+ return function() { _invokeNextHandler(context, idx); }.bindenv(this);
+ }
+
+ //
+ /**
+ * Set a handler (used internally to simplify code).
+ *
+ * @param {string} handlerName - The name of the handler.
+ * @param {function} callback - The function to be assigned to that name.
+ *
+ * @returns {object} The target Rocky.Route instance (this).
+ *
+ * @private
+ */
+ function _setHandler(handlerName, callback) {
+ // Create handler slot if required
+ if (!hasHandler(handlerName)) { _handlers[handlerName] <- null; }
+
+ // Set the handler
+ _handlers[handlerName] = callback;
+
+ return this;
+ }
+}
+
+/**
+ * This class defines a Rocky request context, which combines the core imp API request
+ * and response objects with extracted data (eg. path, authorization), user-defined data,
+ * and housekeeping information (eg. whether the context has responded).
+ *
+ * @copyright Electric Imp, Inc. 2015-19
+ * @license MIT
+ *
+ * @class
+ *
+*/
+
+class Rocky.Context {
+
+ // ------------------ PUBLIC PROPERTIES ------------------//
+ req = null;
+ res = null;
+ sent = null;
+ id = null;
+ time = null;
+ auth = null;
+ path = null;
+ matches = null;
+ timer = null;
+ userdata = null;
+ static _contexts = {};
+
+ /**
+ * The Rock.Context constructor. Not called by user code, only by Rocky instances.
+ *
+ * @constructor
+ *
+ * @param {object} _req - An imp API HTTPRequest instance.
+ * @param {object} _res - An imp API HTTPResponse instance.
+ *
+ * returns {object} A Rocky.Context instance.
+ *
+ */
+ constructor(_req, _res) {
+ req = _req;
+ res = _res;
+ sent = false;
+ time = date();
+ userdata = {};
+
+ // Set the context's identify and then store it
+ do {
+ id = math.rand();
+ } while (id in _contexts);
+ _contexts[id] <- this;
+ }
+
+ //-------------------- STATIC METHODS --------------------//
+
+ /**
+ * Get the context identified by the specified ID.
+ *
+ * @param {integer} id - A Rocky.Context identifier.
+ *
+ * @returns {object} The requested Rocky.Context instance, or null.
+ *
+ */
+ static function get(id) {
+ if (id in _contexts) {
+ return _contexts[id];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Send a response to all current contexts. This closes but does not delete all contexts.
+ *
+ * @param {integer} statuscode - The response's HTTP status code.
+ * @param {string} response - The response body.
+ * @param {table} headers - Optional additional response headers. Default: no additional headers.
+ *
+ */
+ static function sendToAll(statuscode, response, headers = {}) {
+ // Send to all active contexts
+ foreach (key, context in _contexts) {
+ foreach (k, header in headers) {
+ context.setHeader(k, header);
+ }
+ context._doSend(statuscode, response);
+ }
+
+ // Remove all contexts after sending
+ _contexts.clear();
+ }
+
+ //-------------------- PUBLIC METHODS --------------------//
+
+ /**
+ * Does the request include the 'accept' header with the 'text/html' mime type?
+ *
+ * @returns {Bool} true if the request has 'accept: text/html'
+ *
+ */
+ function isbrowser() {
+ return (("accept" in req.headers) && (req.headers.accept.find("text/html") != null));
+ }
+
+ // ADDED 3.0.0
+ // lowerCamelCase version of the above call. Keep the old one to
+ // minimize the incompatibility, but document/recommend the new form
+ function isBrowser() {
+ return isbrowser();
+ }
+
+ /**
+ * Get the value of the specified header.
+ *
+ * @param {string} key - The header name.
+ * @param {string} def - A default value for non-existent headers. Default: null
+ *
+ * @returns {string} The value or the default.
+ *
+ */
+ function getHeader(key, def = null) {
+ key = key.tolower();
+ if (key in req.headers) return req.headers[key];
+ else return def;
+ }
+
+ /**
+ * Set the value of the specified header.
+ *
+ * @param {string} key - The header name.
+ * @param {string} value - The header's assigned value.
+ *
+ */
+ function setHeader(key, value) {
+ return res.header(key, value);
+ }
+
+ /**
+ * Send the context's response. This does not delete the context but
+ * removes it from the store.
+ * Supports two forms: 'send(statuscode, message)' and 'send(message)'.
+ *
+ * @param {integer} code - The response's HTTP status code or its body
+ * @param {string} message - The response's body.
+ * @param {bool} forcejson - Mandate that the body is JSON-encoded.
+ *
+ */
+ function send(code, message = null, forcejson = false) {
+ _doSend(code, message, forcejson);
+
+ // Remove the context from the store
+ if (id in _contexts) delete Rocky.Context._contexts[id];
+ }
+
+ /**
+ * Set the context timeout.
+ *
+ * @param {integer} timeout - The timeout period in seconds.
+ * @param {function} callback - The timeout handler.
+ * @param {function} exceptionHandler - An error handler.
+ *
+ */
+ function setTimeout(timeout, callback = null, exceptionHandler = null) {
+ // Set the timeout timer
+ if (timer) imp.cancelwakeup(timer);
+ timer = imp.wakeup(timeout, function() {
+ if (callback == null) {
+ send(504, "Timeout");
+ } else {
+ try {
+ callback(this);
+ } catch(ex) {
+ if (exceptionHandler != null) exceptionHandler(this, ex);
+ }
+ }
+ }.bindenv(this));
+ }
+
+ /**
+ * Determine if the context's response has been sent.
+ *
+ * @returns {Bool} true if the context's response has been sent, otherwise false.
+ *
+ */
+ function isComplete() {
+ return sent;
+ }
+
+ //-------------------- PRIVATE METHODS --------------------//
+
+ /**
+ * Send the context's response. Handles both 'send(message)' and 'send(code, message)'
+ *
+ * @param {integer/any} code - The response's HTTP status code, or body
+ * @param {any} message - The response's body.
+ * @param {bool} forcejson - Mandate that the body is JSON-encoded. Default: false.
+ *
+ * @private
+ */
+ function _doSend(code, message = null, forcejson = false) {
+ // Cancel the timeout
+ if (timer) {
+ imp.cancelwakeup(timer);
+ timer = null;
+ }
+
+ // Has this context been closed already?
+ if (sent) return false;
+
+ if (forcejson) {
+ // Encode whatever it is as a json object
+ res.header("Content-Type", "application/json; charset=utf-8");
+ res.send(code, http.jsonencode(message));
+ } else if (message == null && typeof code == "integer") {
+ // Empty result code
+ res.send(code, "");
+ } else if (message == null && typeof code == "string") {
+ // No result code, assume 200
+ res.send(200, code);
+ } else if (message == null && (typeof code == "table" || typeof code == "array")) {
+ // No result code, assume 200 ... and encode a json object
+ res.header("Content-Type", "application/json; charset=utf-8");
+ res.send(200, http.jsonencode(code));
+ } else if (typeof code == "integer" && (typeof message == "table" || typeof message == "array")) {
+ // Encode a json object
+ res.header("Content-Type", "application/json; charset=utf-8");
+ res.send(code, http.jsonencode(message));
+ } else {
+ // Normal result
+ res.send(code, message);
+ }
+
+ sent = true;
+ }
+
+}
\ No newline at end of file
diff --git a/Rocky.class.nut b/Rocky.class.nut
deleted file mode 100644
index 4ac5899..0000000
--- a/Rocky.class.nut
+++ /dev/null
@@ -1,653 +0,0 @@
-// Copyright (c) 2015 Electric Imp
-// This file is licensed under the MIT License
-// http://opensource.org/licenses/MIT
-
-const ROCKY_PARSE_ERROR = "Error parsing body of request";
-
-class Rocky {
-
- static VERSION = "2.0.2";
-
- // Route handlers, event handers, and middleware
- _handlers = null;
-
- // Settings:
- _timeout = 10;
- _strictRouting = false;
- _allowUnsecure = false;
- _accessControl = true;
-
- constructor(settings = {}) {
- // Initialize settings
- if ("timeout" in settings) _timeout = settings.timeout;
- if ("allowUnsecure" in settings) _allowUnsecure = settings.allowUnsecure;
- if ("strictRouting" in settings) _strictRouting = settings.strictRouting;
- if ("accessControl" in settings) _accessControl = settings.accessControl;
-
- // Inititalize handlers & middleware
- _handlers = {
- authorize = _defaultAuthorizeHandler.bindenv(this),
- onUnauthorized = _defaultUnauthorizedHandler.bindenv(this),
- onTimeout = _defaultTimeoutHandler.bindenv(this),
- onNotFound = _defaultNotFoundHandler.bindenv(this),
- onException = _defaultExceptionHandler.bindenv(this),
- middlewares = []
- };
-
- // Bind the onrequest handler
- http.onrequest(_onrequest.bindenv(this));
- }
-
- //-------------------- STATIC METHODS --------------------//
- static function getContext(id) {
- return Rocky.Context.get(id);
- }
-
- static function sendToAll(statuscode, response, headers = {}) {
- Rocky.Context.sendToAll(statuscode, response, headers);
- }
-
- //-------------------- PUBLIC METHODS --------------------//
-
- // Requests
- function on(verb, signature, callback, timeout=null) {
- //Check timeout and set it to class-level timeout if not specified for route
- if (timeout == null) {
- timeout = this._timeout;
- }
-
- // Register this signature and verb against the callback
- verb = verb.toupper();
-
- signature = signature.tolower();
- if (!(signature in _handlers)) _handlers[signature] <- {};
-
- local routeHandler = Rocky.Route(callback);
- routeHandler.setTimeout(timeout);
-
- _handlers[signature][verb] <- routeHandler;
-
- return routeHandler;
- }
-
- function post(signature, callback, timeout=null) {
- return on("POST", signature, callback, timeout);
- }
-
- function get(signature, callback, timeout=null) {
- return on("GET", signature, callback, timeout);
- }
-
- function put(signature, callback, timeout=null) {
- return on("PUT", signature, callback, timeout);
- }
-
- // Authorization
- function authorize(callback) {
- _handlers.authorize <- callback;
- return this;
- }
-
- function onUnauthorized(callback) {
- _handlers.onUnauthorized <- callback;
- return this;
- }
-
- // Events
- function onTimeout(callback, t = null) {
- if (t == null) t = _timeout;
-
- _handlers.onTimeout <- callback;
- _timeout = t;
- return this;
- }
-
- function onNotFound(callback) {
- _handlers.onNotFound <- callback;
- return this;
- }
-
- function onException(callback) {
- _handlers.onException <- callback;
- return this;
- }
-
- // Middlewares
- function use(middlewares) {
- if(typeof middlewares == "function") {
- _handlers.middlewares.push(middlewares);
- } else if (typeof _handlers.middlewares == "array") {
- foreach(middleware in middlewares) {
- use(middleware);
- }
- } else {
- throw INVALID_MIDDLEWARE_ERR;
- }
-
- return this;
- }
-
- //-------------------- PRIVATE METHODS --------------------//
- // Adds default access control headers
- function _addAccessControl(res) {
- res.header("Access-Control-Allow-Origin", "*")
- res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
- res.header("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS");
- }
-
- // The HTTP Request Handler
- function _onrequest(req, res) {
- // Add access control headers if required
- if (_accessControl) _addAccessControl(res);
-
- // Setup the context for the callbacks
- local context = Rocky.Context(req, res);
-
- // Check for unsecure reqeusts
- if (_allowUnsecure == false && "x-forwarded-proto" in req.headers && req.headers["x-forwarded-proto"] != "https") {
- context.send(405, "HTTP not allowed.");
- return;
- }
-
- // Parse the request body back into the body
- try {
- req.rawbody <- req.body;
- req.body = _parse_body(req);
- } catch (err) {
- context.send(400, ROCKY_PARSE_ERROR);
- return;
- }
-
- // Look for a handler for this path
- local route = _handler_match(req);
- if (route) {
- // if we have a handler
- context.path = route.path;
- context.matches = route.matches;
-
- // parse auth
- context.auth = _parse_authorization(context);
-
- // Create timeout
- local onTimeout = _handlers.onTimeout;
- local timeout = route.handler.getTimeout();
-
- if (route.handler.hasHandler("onTimeout")) {
- onTimeout = route.handler.getHandler("onTimeout");
- }
-
- context.setTimeout(timeout, onTimeout);
- route.handler.execute(context, _handlers);
- } else {
- // if we don't have a handler
- _handlers.onNotFound(context);
- }
- }
-
- function _parse_body(req) {
- local contentType = "content-type" in req.headers ? req.headers["content-type"] : "";
-
- if (contentType == "application/json" || contentType.find("application/json;") != null) {
- if (req.body == "" || req.body == null) return null;
- return http.jsondecode(req.body);
- }
-
- if (contentType == "application/x-www-form-urlencoded" || contentType.find("application/x-www-form-urlencoded;") != null) {
- if (req.body == "" || req.body == null) return null;
- return http.urldecode(req.body);
- }
-
- if (contentType.find("multipart/form-data;") == 0) {
- local parts = [];
-
- // Find the boundary in the contentType
- local boundary;
- local findString = regexp(@"boundary([ ]*)=");
- local match = findString.search(contentType);
-
- if (match) {
- boundary = contentType.slice(match.end);
- boundary = strip(boundary);
- } else {
- throw "No boundary found in content-type";
- }
-
- // Remove all carriage returns from string (to support either \r\n or \n for linebreaks)
- local body = "";
- local bodyLines = split(req.body, "\r");
- foreach (i, line in bodyLines) {
- body += line;
- }
-
- // Find all boundaries in the body
- local boundaries = [];
- local bregex = regexp2(@"--" + boundary);
- local bmatch = bregex.search(body);
- while (bmatch != null) {
- boundaries.push(bmatch);
- bmatch = bregex.search(body, bmatch.begin+1);
- }
-
- // Create array of parts
- for (local i = 0; i < boundaries.len() - 1; i++) {
- // Extract one part from the request
- local part = body.slice(boundaries[i].end + 1, boundaries[i+1].begin);
-
- // Split the part into headers and body
- local partSplit = regexp2("\n\n").search(part);
- local header = part.slice(0, partSplit.begin);
- local data = part.slice(partSplit.end, -1);
-
- // Create table to store the parsed content of the part
- local parsedPart = {};
- parsedPart.name <- null;
- parsedPart.data <- data;
-
- // Extract each individual header and store in parsedPart
- local keyValueRegex = regexp2(@"(^|\W)(\S+)\s*[=:]\s*(""[^""]*""|\S*)");
- local keyValueCapture = keyValueRegex.capture(header);
- while (keyValueCapture != null) {
- // Extract key and value for the header
- local key = header.slice(keyValueCapture[2].begin, keyValueCapture[2].end);
- local val = header.slice(keyValueCapture[3].begin, keyValueCapture[3].end);
- // Remove any quotations
- if (val[0] == '"') {
- val = val.slice(1, -1);
- }
- // Save the header in parsedPart
- parsedPart[key] <- val;
-
- keyValueCapture = keyValueRegex.capture(header, keyValueCapture[0].end);
- }
-
- // Add the parsed part to the array of parts
- parts.push(parsedPart);
- }
-
- // Return the array of parts
- return parts;
- }
-
- // Nothing matched, send back the original body
- return req.body;
- }
-
- function _parse_authorization(context) {
- if ("authorization" in context.req.headers) {
- local auth = split(context.req.headers.authorization, " ");
-
- if (auth.len() == 2 && auth[0] == "Basic") {
- // Note the username and password can't have colons in them
- local creds = http.base64decode(auth[1]).tostring();
- creds = split(creds, ":");
- if (creds.len() == 2) {
- return { authType = "Basic", user = creds[0], pass = creds[1] };
- }
- } else if (auth.len() == 2 && auth[0] == "Bearer") {
- // The bearer is just the password
- if (auth[1].len() > 0) {
- return { authType = "Bearer", user = auth[1], pass = auth[1] };
- }
- }
- }
-
- return { authType = "None", user = "", pass = "" };
- }
-
- function _extract_parts(routeHandler, path, regexp = null) {
- local parts = { path = [], matches = [], handler = routeHandler };
-
- // Split the path into parts
- foreach (part in split(path, "/")) {
- parts.path.push(part);
- }
-
- // Capture regular expression matches
- if (regexp != null) {
- local caps = regexp.capture(path);
- local matches = [];
- foreach (cap in caps) {
- parts.matches.push(path.slice(cap.begin, cap.end));
- }
- }
-
- return parts;
- }
-
- function _handler_match(req) {
- local signature = req.path.tolower();
- local verb = req.method.toupper();
-
- // ignore trailing /s if _strictRouting == false
- if(!_strictRouting) {
- while (signature.len() > 1 && signature[signature.len()-1] == '/') {
- signature = signature.slice(0, signature.len()-1);
- }
- }
-
- if ((signature in _handlers) && (verb in _handlers[signature])) {
- // We have an exact signature match
- return _extract_parts(_handlers[signature][verb], signature);
- } else if ((signature in _handlers) && ("*" in _handlers[signature])) {
- // We have a partial signature match
- return _extract_parts(_handlers[signature]["*"], signature);
- } else {
- // Let's iterate through all handlers and search for a regular expression match
- foreach (_signature,_handler in _handlers) {
- if (typeof _handler == "table") {
- foreach (_verb,_callback in _handler) {
- if (_verb == verb || _verb == "*") {
- try {
- local ex = regexp(_signature);
- if (ex.match(signature)) {
- // We have a regexp handler match
- return _extract_parts(_callback, signature, ex);
- }
- } catch (e) {
- // Don't care about invalid regexp.
- }
- }
- }
- }
- }
- }
- return null;
- }
-
- //-------------------- DEFAULT HANDLERS --------------------//
- function _defaultAuthorizeHandler(context) {
- return true;
- }
-
- function _defaultUnauthorizedHandler(context) {
- context.send(401, "Unauthorized");
- }
-
- function _defaultNotFoundHandler(context) {
- context.send(404, format("No handler for %s %s", context.req.method, context.req.path));
- }
-
- function _defaultTimeoutHandler(context) {
- context.send(500, format("Agent Request timed out after %i seconds.", _timeout));
- }
-
- function _defaultExceptionHandler(context, ex) {
- server.error(ex);
- context.send(500, "Agent Error: " + ex);
- }
-
-}
-
-class Rocky.Route {
- _handlers = null;
- _timeout = null;
- _callback = null;
-
- constructor(callback) {
- _handlers = {
- middlewares = []
- };
- _timeout = 10;
- _callback = callback;
- }
-
- //-------------------- PUBLIC METHODS --------------------//
- function execute(context, defaultHandlers) {
- // setup handlers
- // NOTE: Copying these handlers into the route might have some unintended side effect.
- // Consider changing this if issues come up.
- foreach (handlerName, handler in defaultHandlers) {
- // Copy over the non-middleware handlers
- if (handlerName != "middlewares") {
- if (!hasHandler(handlerName)) { _setHandler(handlerName, handler); }
- } else {
- // Copy the handlers over so we can iterate through in
- // the correct order:
- for (local i = handler.len() -1; i >= 0; i--) {
- // Only add handlers that we haven't already added
- if (_handlers.middlewares.find(handler[i]) == null) {
- _handlers.middlewares.insert(0, handler[i]);
- }
- }
- }
- }
-
- // Run all the handlers
- _invokeNextHandler(context);
- }
-
- function authorize(callback) {
- return _setHandler("authorize", callback);
- }
-
- function onException(callback) {
- return _setHandler("onException", callback);
- }
-
- function onUnauthorized(callback) {
- return _setHandler("onUnauthorized", callback);
- }
-
- function onTimeout(callback, t = null) {
- if (t == null) t = _timeout;
- _timeout = t;
-
- return _setHandler("onTimeout", callback);
- }
-
- function use(middlewares) {
- if (!hasHandler("middlewares")) { _handlers["middlewares"] <- [] };
-
- if(typeof middlewares == "function") {
- _handlers.middlewares.push(middlewares);
- } else if (typeof _handlers.middlewares == "array") {
- foreach(middleware in middlewares) {
- use(middleware);
- }
- } else {
- throw INVALID_MIDDLEWARE_ERR;
- }
-
- return this;
- }
-
- function hasHandler(handlerName) {
- return (handlerName in _handlers);
- }
-
- function getHandler(handlerName) {
- // Return null if no handler
- if (!hasHandler(handlerName)) { return null; }
-
- // Return the handler if it exists
- return _handlers[handlerName];
- }
-
- function getTimeout() {
- return _timeout;
- }
-
- function setTimeout(timeout) {
- return _timeout = timeout;
- }
-
- //-------------------- PRIVATE METHODS --------------------//
-
- // Invokes the next middleware, and moves on the
- // authorize/callback/onUnauthorized flow when done with middlewares
- function _invokeNextHandler(context, idx = 0) {
- // If we've sent a response, we're done
- if (context.isComplete()) return;
-
- // check if we have middlewares left to execute
- if (idx < _handlers.middlewares.len()) {
- try {
- // if we do, execute them (with a next() function for the next middleware)
- _handlers.middlewares[idx](context, _nextGenerator(context, idx+1));
- } catch (ex) {
- _handlers.onException(context, ex);
- }
- } else {
- // Otherwise, run the rest of the flow
- try {
- // Check if we're authorized
- if (_handlers.authorize(context)) {
- // If we're authorized, execute the route handler
- _callback(context);
- } else {
- // if we unauthorized, execute the onUnauthorized handler
- _handlers.onUnauthorized(context);
- }
- } catch (ex) {
- _handlers.onException(context, ex);
- }
- }
- }
-
- // Generator method to create next() functions for middleware
- function _nextGenerator(context, idx) {
- return function() { _invokeNextHandler(context, idx); }.bindenv(this);
- }
-
-
- // Sets a handlers (used internally to simplify code)
- function _setHandler(handlerName, callback) {
- // Create handler slot if required
- if (!hasHandler(handlerName)) { _handlers[handlerName] <- null; }
-
- // Set the handler
- _handlers[handlerName] = callback;
-
- return this;
- }
-}
-
-class Rocky.Context {
- req = null;
- res = null;
- sent = null;
- id = null;
- time = null;
- auth = null;
- path = null;
- matches = null;
- timer = null;
- userdata = null;
- static _contexts = {};
-
- constructor(_req, _res) {
- req = _req;
- res = _res;
- sent = false;
- time = date();
- userdata = {};
-
- // Identify and store the context
- do {
- id = math.rand();
- } while (id in _contexts);
- _contexts[id] <- this;
- }
-
- //-------------------- STATIC METHODS --------------------//
- static function get(id) {
- if (id in _contexts) {
- return _contexts[id];
- } else {
- return null;
- }
- }
-
- // Closes ALL contexts
- static function sendToAll(statuscode, response, headers = {}) {
- // Send to all active contexts
- foreach (key, context in _contexts) {
- foreach (k, header in headers) {
- context.setHeader(k, header);
- }
- context._doSend(statuscode, response);
- }
- // Remove all contexts after sending
- _contexts.clear();
- }
-
- //-------------------- PUBLIC METHODS --------------------//
- function isbrowser() {
- return (("accept" in req.headers) && (req.headers.accept.find("text/html") != null));
- }
-
- function getHeader(key, def = null) {
- key = key.tolower();
- if (key in req.headers) return req.headers[key];
- else return def;
- }
-
- function setHeader(key, value) {
- return res.header(key, value);
- }
-
- function send(code, message = null, forcejson = false) {
- _doSend(code, message, forcejson);
-
- // Remove the context from the store
- if (id in _contexts) {
- delete Rocky.Context._contexts[id];
- }
- }
-
- function setTimeout(timeout, callback) {
- // Set the timeout timer
- if (timer) imp.cancelwakeup(timer);
- timer = imp.wakeup(timeout, function() {
- if (callback == null) {
- send(502, "Timeout");
- } else {
- callback(this);
- }
- }.bindenv(this))
- }
-
- function isComplete() {
- return sent;
- }
-
- //-------------------- PRIVATE METHODS --------------------//
-
- function _doSend(code, message = null, forcejson = false) {
- // Cancel the timeout
- if (timer) {
- imp.cancelwakeup(timer);
- timer = null;
- }
-
- // Has this context been closed already?
- if (sent) {
- return false;
- }
-
- if (forcejson) {
- // Encode whatever it is as a json object
- res.header("Content-Type", "application/json; charset=utf-8");
- res.send(code, http.jsonencode(message));
- } else if (message == null && typeof code == "integer") {
- // Empty result code
- res.send(code, "");
- } else if (message == null && typeof code == "string") {
- // No result code, assume 200
- res.send(200, code);
- } else if (message == null && (typeof code == "table" || typeof code == "array")) {
- // No result code, assume 200 ... and encode a json object
- res.header("Content-Type", "application/json; charset=utf-8");
- res.send(200, http.jsonencode(code));
- } else if (typeof code == "integer" && (typeof message == "table" || typeof message == "array")) {
- // Encode a json object
- res.header("Content-Type", "application/json; charset=utf-8");
- res.send(code, http.jsonencode(message));
- } else {
- // Normal result
- res.send(code, message);
- }
- sent = true;
- }
-
-}
diff --git a/Secure-RGBLed-Example/secureRGB.agent.nut b/Secure-RGBLed-Example/secureRGB.agent.nut
index 1f40956..1b343e6 100644
--- a/Secure-RGBLed-Example/secureRGB.agent.nut
+++ b/Secure-RGBLed-Example/secureRGB.agent.nut
@@ -1,10 +1,10 @@
-// Copyright (c) 2015 Electric Imp
+// Copyright (c) 2015-19 Electric Imp
// This file is licensed under the MIT License
// http://opensource.org/licenses/MIT
-#require "Rocky.class.nut:2.0.0"
+#require "Rocky.agent.lib.nut:3.0.0"
-app <- Rocky();
+app <- Rocky.init();
/******************** User Access Control ********************/
// You should change the SALT to something unique
@@ -36,7 +36,7 @@ function saveUAC() {
// Functions for checking User Access Control
-function checkAccess(user,pass, access) {
+function checkAccess(user, pass, access) {
return (user in uac && uac[user].pass == passwordHash(pass) && uac[user].access.find(access) != null);
}
@@ -174,7 +174,8 @@ app.post("/color", function(context) {
// if preflight check passed - do things
led.color = context.req.body.color;
- device.send("setColor", context.req.body.color);
+ server.log("Setting color to R: " + led.color.red + ", G: " + led.color.green + ", B: " + led.color.blue);
+ device.send("setColor", led.color);
// send the response
context.send({ "verb": "POST", "led": led });
@@ -195,7 +196,8 @@ app.post("/state", function(context) {
// if preflight check passed - do things
led.state = context.req.body.state;
- device.send("setState", context.req.body.state);
+ server.log("Setting state to " + (led.state ? "on" : "off"));
+ device.send("setState", led.state);
// send the response
context.send({ "verb": "POST", "led": led });
diff --git a/Secure-RGBLed-Example/secureRGB.device.nut b/Secure-RGBLed-Example/secureRGB.device.nut
index c36b571..9e46292 100644
--- a/Secure-RGBLed-Example/secureRGB.device.nut
+++ b/Secure-RGBLed-Example/secureRGB.device.nut
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Electric Imp
+// Copyright (c) 2015-19 Electric Imp
// This file is licensed under the MIT License
// http://opensource.org/licenses/MIT
diff --git a/tests/Core.nut b/tests/Core.nut
index 52d59e2..b6016d9 100644
--- a/tests/Core.nut
+++ b/tests/Core.nut
@@ -25,7 +25,7 @@
// while class being tested can be accessed from global scope as "::Promise".
class Core extends ImpTestCase {
-
+
// Default params for createTest function
defaultParams = {
// Options for Rocky constructor
@@ -62,7 +62,7 @@ class Core extends ImpTestCase {
"cb": null,
// If true, defaultParams.cb will be forced to be null
"cbUseNull": false,
- // Callback, that will be called, when server will receive new request.
+ // Callback, that will be called, when server will receive new request.
// If not specified, then server will respond with 200 statuscode.
// If params.cb specified, then this callback will have no affect.
"callback": null,
@@ -103,7 +103,7 @@ class Core extends ImpTestCase {
// 8) Send one (or more) request(s) and wait for Rocky's response.
// When get response, verify statuscode and call callback function for additional verify (if defined). If everything ok, then test passed, otherwise failed.
// If more than one requests send, then test will be passed only when all responses verifications passed.
- //
+ //
// @param {table} params
// @param {string} expect - Expected completion of the test (success|fail)
// @return {Promise}
@@ -126,8 +126,8 @@ class Core extends ImpTestCase {
local app;
local route;
params.setdelegate(defaultParams); // Setup default values
- _createTestCallConstructorsDirectly(params, fail) &&
- _createTestSetupHandlers(params, fail, app, route) &&
+ _createTestCallConstructorsDirectly(params, fail) &&
+ _createTestSetupHandlers(params, fail, app, route) &&
_createTestSendRequest(params, fail, success, expect);
}.bindenv(this));
}
@@ -182,7 +182,7 @@ class Core extends ImpTestCase {
function _createTestSetupHandlers(params, fail, app, route) {
try {
// Setup Rocky handlers
- app = Rocky(params.params);
+ app = Rocky.init(params.params);
if (params.onAuthorize != null) {
app.authorize(params.onAuthorize);
}
@@ -272,8 +272,8 @@ class Core extends ImpTestCase {
local signature = typeof params.signatureOverride == "string" ? params.signatureOverride : params.signature;
local req = http.request(
method,
- http.agenturl() + signature,
- params.headers,
+ http.agenturl() + signature,
+ params.headers,
params.body
);
req.sendasync(function(res) {
@@ -312,7 +312,7 @@ class Core extends ImpTestCase {
// createTestAll
// Create Promise for series of tests testing Rocky
- //
+ //
// @param {array} tests - Array of 'params' for createTest(params)
// @param {string} type - The condition for the success of all tests (positive|negative)
// @return {Promise}
diff --git a/tests/CoreRockyMethod.nut b/tests/CoreRockyMethod.nut
index dd858c8..df2d6db 100644
--- a/tests/CoreRockyMethod.nut
+++ b/tests/CoreRockyMethod.nut
@@ -32,7 +32,7 @@
class CoreRockyMethod extends Core {
@include __PATH__+"/CoreHandlers.nut"
-
+
withoutBody = false;
function setUp() {
@@ -42,14 +42,14 @@ class CoreRockyMethod extends Core {
function testSimple() {
return createTest({
- "signature": "/testSimple",
+ "signature": "/testSimple",
"method": getVerb()
});
}
function testSimpleStrict() {
return createTest({
- "signature": "/testSimple",
+ "signature": "/testSimple",
"method": getVerb(),
"methodStrictUsage": true
});
@@ -57,10 +57,10 @@ class CoreRockyMethod extends Core {
function testFull() {
return createTest({
- "signature": "/testFull",
+ "signature": "/testFull",
"method": getVerb(),
"headers": {
- "testFull": "testFull",
+ "testFull": "testFull",
"GenericVerbCompatibilityTest": true
},
"body": "testFull body"
@@ -69,17 +69,17 @@ class CoreRockyMethod extends Core {
function testSimpleWOSignature() {
return createTest({
- "signature": "/",
+ "signature": "/",
"method": getVerb()
});
}
function testWOSignatureFull() {
return createTest({
- "signature": "/",
+ "signature": "/",
"method": getVerb(),
"headers": {
- "testWOSignatureFull": 35,
+ "testWOSignatureFull": 35,
"GenericVerbCompatibilityTest": false
},
"body": "testWOSignatureFull body"
@@ -88,10 +88,10 @@ class CoreRockyMethod extends Core {
function testTimeout() {
return createTest({
- "signature": "/testTimeout",
+ "signature": "/testTimeout",
"method": getVerb(),
"headers": {
- "testTimeout": 35,
+ "testTimeout": 35,
"GenericVerbCompatibilityTest": "testTimeout"
},
"body": "testTimeout body",
@@ -101,7 +101,7 @@ class CoreRockyMethod extends Core {
function testSimpleRegexp_1() {
return createTest({
- "signature": ".*",
+ "signature": ".*",
"signatureOverride": "/testSimpleRegexp_1",
"method": getVerb()
});
@@ -109,7 +109,7 @@ class CoreRockyMethod extends Core {
function testSimpleRegexp_2() {
return createTest({
- "signature": "/test1(.*/test\\d.*)",
+ "signature": "/test1(.*/test\\d.*)",
"signatureOverride": "/test1/test2/test3",
"method": getVerb(),
"callback": function(context) {
@@ -130,21 +130,21 @@ class CoreRockyMethod extends Core {
function testContentTypeJson() {
return contentType({
- "contentType": "contentType",
+ "contentType": "contentType",
"content-type": "application/json"
}, "application/json", http.jsonencode({"contentType": "body"}));
}
function testContentTypeForm() {
return contentType({
- "contentType": "contentType",
+ "contentType": "contentType",
"content-type": "application/x-www-form-urlencoded"
}, "application/x-www-form-urlencoded", "contentType=body");
}
function testContentTypeMultipart() {
return Promise(function(resolve, reject) {
- local app = Rocky();
+ local app = Rocky.init();
local headers = {
"Content-Type": "multipart/form-data; boundary=----------287032381131322"
@@ -215,7 +215,7 @@ GIF89a.............,...........D..;
function contentType(headers, contentType, body) {
local params = {
- "signature": "/contentType",
+ "signature": "/contentType",
"method": getVerb(),
"headers": headers,
"callback": function(context) {
@@ -272,8 +272,8 @@ GIF89a.............,...........D..;
function testQuery() {
return createTest({
- "signature": "/testQuery",
- "signatureOverride": "/testQuery?first=1&second=2",
+ "signature": "/testQuery",
+ "signatureOverride": "/testQuery?first=1&second=2",
"method": getVerb(),
"callback": function(context) {
try {
diff --git a/tests/RockyConstructor.agent.test.nut b/tests/RockyConstructor.agent.test.nut
index 55775ca..7496dd2 100644
--- a/tests/RockyConstructor.agent.test.nut
+++ b/tests/RockyConstructor.agent.test.nut
@@ -33,7 +33,7 @@ class RockyConstructor extends Core {
@include __PATH__+"/CoreHandlers.nut"
values = null;
-
+
function setUp() {
values = [null, true, 0, -1, 1, 13.37, "String", [1, 2], {"counter": "this"}, blob(64), function(){}];
}
@@ -42,7 +42,7 @@ class RockyConstructor extends Core {
local tests = [];
foreach (element in values) {
tests.push({
- "signature": "/testAccessControlOption",
+ "signature": "/testAccessControlOption",
"params": {"accessControl": element}
});
}
@@ -53,7 +53,7 @@ class RockyConstructor extends Core {
local tests = [];
foreach (element in values) {
tests.push({
- "signature": "/testAllowUnsecureOption",
+ "signature": "/testAllowUnsecureOption",
"params": {"allowUnsecure": element}
});
}
@@ -64,25 +64,41 @@ class RockyConstructor extends Core {
local tests = [];
foreach (element in values) {
tests.push({
- "signature": "/testStrictRoutingOption",
+ "signature": "/testStrictRoutingOption",
"params": {"strictRouting": element}
});
}
return createTestAll(tests);
}
- // issue: https://github.com/electricimp/Rocky/issues/24
- //function testRockyTimeoutOption() {
- // local tests = [];
- // foreach (element in values) {
- // tests.push({
- // "signature": "/testTimeoutOption",
- // "params": {"timeout": element},
- // "onException": onException.bindenv(this)
- // });
- // }
- // return createTestAll(tests);
- //}
+ // issue: https://github.com/electricimp/Rocky/issues/24 (and 23)
+ function testRockyTimeoutOptionBad() {
+ // These should FAIL
+ local tests = [];
+ foreach (idx, element in values) {
+ if (idx < 2 && idx > 5) {
+ tests.push({
+ "signature": "/testTimeoutOption",
+ "params": {"timeout": element},
+ });
+ }
+ }
+ return createTestAll(tests, "negative");
+ }
+
+ function testRockyTimeoutOptionGood() {
+ // These should PASS
+ local tests = [];
+ foreach (idx, element in values) {
+ if (idx > 1 && idx < 6) {
+ tests.push({
+ "signature": "/testTimeoutOption",
+ "params": {"timeout": element},
+ });
+ }
+ }
+ return createTestAll(tests);
+ }
function testRockyWrongOption() {
local values = ["hello", "strictrouting", "AllowUnsecure", "TimeOut"];
@@ -92,8 +108,9 @@ class RockyConstructor extends Core {
params[element] <- c++;
}
return createTest({
- "signature": "/testWrongOption",
+ "signature": "/testWrongOption",
"params": params
});
}
+
}
diff --git a/tests/RockyHandlers.agent.test.nut b/tests/RockyHandlers.agent.test.nut
index 178924e..b75e797 100644
--- a/tests/RockyHandlers.agent.test.nut
+++ b/tests/RockyHandlers.agent.test.nut
@@ -102,19 +102,20 @@ class RockyHandlers extends Core {
}
// issue: https://github.com/electricimp/Rocky/issues/25
- //function testTimeoutException() {
- // info("This test will take a couple of seconds");
- // return createTest({
- // "signature": "/testTimeout",
- // "timeout": true,
- // "onTimeout": throwException.bindenv(this),
- // "onException": onException.bindenv(this),
- // "statuscode": 500
- // });
- //}
+ function testTimeoutException() {
+ info("This test will take a couple of seconds");
+ return createTest({
+ "signature": "/testTimeout",
+ "timeout": true,
+ "onTimeout": throwException.bindenv(this),
+ "onException": onException.bindenv(this),
+ "statuscode": 500
+ });
+ }
function testNotFound() {
return createTest({
+ "params": { "sigCaseSensitive" : true },
"signature": "/testNotFound",
"signatureOverride": "/testNotFoundIDontExist",
"onNotFound": onNotFound.bindenv(this),
@@ -122,14 +123,65 @@ class RockyHandlers extends Core {
});
}
+ // issue: https://github.com/electricimp/Rocky/issues/36
+ function testNotFound2a() {
+ return createTest({
+ "params": { "sigCaseSensitive" : true },
+ "signature": "/testNotFound",
+ "signatureOverride": "/testnotfound",
+ "onNotFound": onNotFound.bindenv(this),
+ "statuscode": 404
+ });
+ }
+
+ function testNotFound2b() {
+ return createTest({
+ "params": { "sigCaseSensitive" : true },
+ "signature": "/testNotFound",
+ "signatureOverride": "/TestnotFound",
+ "onNotFound": onNotFound.bindenv(this),
+ "statuscode": 404
+ });
+ }
+
+ function testNotFound2c() {
+ return createTest({
+ "params": { "sigCaseSensitive" : true },
+ "signature": "/testNotFound",
+ "signatureOverride": "/TestNotfound",
+ "onNotFound": onNotFound.bindenv(this),
+ "statuscode": 404
+ });
+ }
+
+ function testNotFound2d() {
+ return createTest({
+ "params": { "sigCaseSensitive" : true },
+ "signature": "/testNotFound",
+ "signatureOverride": "/TESTNOTFOUND",
+ "onNotFound": onNotFound.bindenv(this),
+ "statuscode": 404
+ });
+ }
+
+ function testNotFound2e() {
+ return createTest({
+ "params": { "sigCaseSensitive" : true },
+ "signature": "/testNotFound",
+ "signatureOverride": "/testNotFound",
+ "onNotFound": onNotFound.bindenv(this),
+ "statuscode": 404
+ }, "fail");
+ }
+
// issue: https://github.com/electricimp/Rocky/issues/25
- //function testNotFoundException() {
- // return createTest({
- // "signature": "/testNotFoundException",
- // "signatureOverride": "/testNotFoundIDontExist",
- // "onNotFound": throwException.bindenv(this),
- // "onException": onException.bindenv(this),
- // "statuscode": 500
- // });
- //}
+ function testNotFoundException() {
+ return createTest({
+ "signature": "/testNotFoundException",
+ "signatureOverride": "/testNotFoundIDontExist",
+ "onNotFound": throwException.bindenv(this),
+ "onException": onException.bindenv(this),
+ "statuscode": 500
+ });
+ }
}
diff --git a/tests/RockyRouteHandlers.agent.test.nut b/tests/RockyRouteHandlers.agent.test.nut
index 84b486a..3c81908 100644
--- a/tests/RockyRouteHandlers.agent.test.nut
+++ b/tests/RockyRouteHandlers.agent.test.nut
@@ -102,14 +102,14 @@ class RockyRouteHandlers extends Core {
}
// issue: https://github.com/electricimp/Rocky/issues/25
- //function testTimeoutException() {
- // info("This test will take a couple of seconds");
- // return createTest({
- // "signature": "/testTimeout",
- // "timeout": true,
- // "onTimeoutRoute": throwException.bindenv(this).bindenv(this),
- // "onExceptionRoute": onException.bindenv(this),
- // "statuscode": 500
- // });
- //}
+ function testTimeoutException() {
+ info("This test will take a couple of seconds");
+ return createTest({
+ "signature": "/testTimeoutException",
+ "timeout": true,
+ "onTimeoutRoute": throwException.bindenv(this),
+ "onExceptionRoute": onException.bindenv(this),
+ "statuscode": 500
+ });
+ }
}