diff --git a/examples/0-hello-world/src/app.gleam b/examples/0-hello-world/src/app.gleam
index 2511536..51907f9 100644
--- a/examples/0-hello-world/src/app.gleam
+++ b/examples/0-hello-world/src/app.gleam
@@ -16,7 +16,7 @@ pub fn middleware(
service: fn(wisp.Request) -> wisp.Response,
) -> wisp.Response {
let req = wisp.method_override(req)
- use <- wisp.log_requests(req)
+ use <- wisp.log_request(req)
use <- wisp.rescue_crashes
service(req)
diff --git a/src/wisp.gleam b/src/wisp.gleam
index eaf6b66..b2681cf 100644
--- a/src/wisp.gleam
+++ b/src/wisp.gleam
@@ -23,17 +23,30 @@ import mist
// Running the server
//
-// TODO: test
-// TODO: document
+/// Convert a Wisp request handler into a function that can be run with the Mist
+/// web server.
+///
+/// # Examples
+///
+/// ```gleam
+/// pub fn main() {
+/// let assert Ok(_) =
+/// wisp.mist_service(handle_request)
+/// |> mist.new
+/// |> mist.port(8000)
+/// |> mist.start_http
+/// process.sleep_forever()
+/// }
+/// ```
pub fn mist_service(
- service: fn(Request) -> Response,
+ handler: fn(Request) -> Response,
) -> fn(HttpRequest(mist.Connection)) -> HttpResponse(mist.ResponseData) {
fn(request: HttpRequest(_)) {
let connection = make_connection(mist_body_reader(request))
let request = request.set_body(request, connection)
let response =
request
- |> service
+ |> handler
|> mist_response
// TODO: use some FFI to ensure this always happens, even if there is a crash
@@ -90,43 +103,114 @@ fn mist_send_file(path: String) -> mist.ResponseData {
// Responses
//
-pub type ResponseBody {
- Empty
- // TODO: remove content type
- File(path: String)
+/// The body of a HTTP response, to be sent to the client.
+///
+pub type Body {
+ /// A body of unicode text.
+ ///
+ /// The body is represented using a `StringBuilder`. If you have a `String`
+ /// you can use the `string_builder.from_string` function to convert it.
+ ///
Text(StringBuilder)
+ /// A body of the contents of a file.
+ ///
+ /// This will be sent efficiently using the `send_file` function of the
+ /// underlying HTTP server. The file will not be read into memory so it is
+ /// safe to send large files this way.
+ ///
+ File(path: String)
+ /// An empty body. This may be returned by the `require_*` middleware
+ /// functions in the event of a failure, invalid request, or other situation
+ /// in which the request cannot be processed.
+ ///
+ /// Your application may wish to use a middleware to provide default responses
+ /// in place of any with an empty body.
+ ///
+ Empty
}
-/// An alias for a HTTP response containing a `ResponseBody`.
+/// An alias for a HTTP response containing a `Body`.
pub type Response =
- HttpResponse(ResponseBody)
+ HttpResponse(Body)
-// TODO: document
+/// Create an empty response with the given status code.
+///
+/// # Examples
+///
+/// ```gleam
+/// response(200)
+/// // -> Response(200, [], Empty)
+/// ```
+///
pub fn response(status: Int) -> Response {
HttpResponse(status, [], Empty)
}
-// TODO: test
-// TODO: document
-pub fn set_body(response: Response, body: ResponseBody) -> Response {
+/// Set the body of a response.
+///
+/// # Examples
+///
+/// ```gleam
+/// response(200)
+/// |> set_body(File("/tmp/myfile.txt"))
+/// // -> Response(200, [], File("/tmp/myfile.txt"))
+/// ```
+///
+pub fn set_body(response: Response, body: Body) -> Response {
response
|> response.set_body(body)
}
-// TODO: test
-// TODO: document
+/// Create a HTML response.
+///
+/// The body is expected to be valid HTML, though this is not validated.
+/// The `content-type` header will be set to `text/html`.
+///
+/// # Examples
+///
+/// ```gleam
+/// let body = string_builder.from_string("
Hello, Joe!
")
+/// html_response(body, 200)
+/// // -> Response(200, [#("content-type", "text/html")], Text(body))
+/// ```
+///
pub fn html_response(html: StringBuilder, status: Int) -> Response {
HttpResponse(status, [#("content-type", "text/html")], Text(html))
}
-// TODO: document
+/// Set the body of a response to a given HTML document, and set the
+/// `content-type` header to `text/html`.
+///
+/// The body is expected to be valid HTML, though this is not validated.
+///
+/// # Examples
+///
+/// ```gleam
+/// let body = string_builder.from_string("
Hello, Joe!
")
+/// response(201)
+/// |> html_body(body)
+/// // -> Response(201, [#("content-type", "text/html")], Text(body))
+/// ```
+///
pub fn html_body(response: Response, html: StringBuilder) -> Response {
response
|> response.set_body(Text(html))
|> response.set_header("content-type", "text/html")
}
-// TODO: document
+/// Create an empty response with status code 405: Method Not Allowed. Use this
+/// when a request does not have an appropriate method to be handled.
+///
+/// The `allow` header will be set to a comma separated list of the permitted
+/// methods.
+///
+/// # Examples
+///
+/// ```gleam
+/// method_not_allowed([Get, Post])
+/// // -> Response(405, [#("allow", "GET, POST")], Empty)
+/// ```
+///
pub fn method_not_allowed(permitted: List(Method)) -> Response {
let allowed =
permitted
@@ -137,42 +221,106 @@ pub fn method_not_allowed(permitted: List(Method)) -> Response {
HttpResponse(405, [#("allow", allowed)], Empty)
}
-// TODO: document
+/// Create an empty response with status code 200: OK.
+///
+/// # Examples
+///
+/// ```gleam
+/// ok()
+/// // -> Response(200, [], Empty)
+/// ```
+///
pub fn ok() -> Response {
HttpResponse(200, [], Empty)
}
-// TODO: document
+/// Create an empty response with status code 201: Created.
+///
+/// # Examples
+///
+/// ```gleam
+/// created()
+/// // -> Response(201, [], Empty)
+/// ```
+///
pub fn created() -> Response {
HttpResponse(201, [], Empty)
}
-// TODO: document
+/// Create an empty response with status code 202: Accepted.
+///
+/// # Examples
+///
+/// ```gleam
+/// created()
+/// // -> Response(202, [], Empty)
+/// ```
+///
pub fn accepted() -> Response {
HttpResponse(202, [], Empty)
}
-// TODO: document
+/// Create an empty response with status code 204: No content.
+///
+/// # Examples
+///
+/// ```gleam
+/// no_content()
+/// // -> Response(204, [], Empty)
+/// ```
+///
pub fn no_content() -> Response {
HttpResponse(204, [], Empty)
}
-// TODO: document
+/// Create an empty response with status code 404: No content.
+///
+/// # Examples
+///
+/// ```gleam
+/// not_found()
+/// // -> Response(404, [], Empty)
+/// ```
+///
pub fn not_found() -> Response {
HttpResponse(404, [], Empty)
}
-// TODO: document
+/// Create an empty response with status code 400: Bad request.
+///
+/// # Examples
+///
+/// ```gleam
+/// bad_request()
+/// // -> Response(400, [], Empty)
+/// ```
+///
pub fn bad_request() -> Response {
HttpResponse(400, [], Empty)
}
-// TODO: document
+/// Create an empty response with status code 413: Entity too large.
+///
+/// # Examples
+///
+/// ```gleam
+/// entity_too_large()
+/// // -> Response(413, [], Empty)
+/// ```
+///
pub fn entity_too_large() -> Response {
HttpResponse(413, [], Empty)
}
-// TODO: document
+/// Create an empty response with status code 500: Internal server error.
+///
+/// # Examples
+///
+/// ```gleam
+/// internal_server_error()
+/// // -> Response(500, [], Empty)
+/// ```
+///
pub fn internal_server_error() -> Response {
HttpResponse(500, [], Empty)
}
@@ -181,6 +329,11 @@ pub fn internal_server_error() -> Response {
// Requests
//
+/// The connection to the client for a HTTP request.
+///
+/// The body of the request can be read from this connection using functions
+/// such as `require_multipart_body`.
+///
pub opaque type Connection {
Connection(
reader: Reader,
@@ -245,43 +398,87 @@ type Read {
ReadingFinished
}
-// TODO: document
+/// Set the maximum permitted size of a request body of the request in bytes.
+///
+/// If a body is larger than this size attempting to read the body will result
+/// in a response with status code 413: Entity too large will be returned to the
+/// client.
+///
+/// This limit only applies for headers and bodies that get read into memory.
+/// Part of a multipart body that contain files and so are streamed to disc
+/// instead use the `max_files_size` limit.
+///
pub fn set_max_body_size(request: Request, size: Int) -> Request {
Connection(..request.body, max_body_size: size)
|> request.set_body(request, _)
}
-// TODO: document
+/// Get the maximum permitted size of a request body of the request in bytes.
+///
pub fn get_max_body_size(request: Request) -> Int {
request.body.max_body_size
}
-// TODO: document
+/// Set the maximum permitted size of all files uploaded by a request, in bytes.
+///
+/// If a request contains fails which are larger in total than this size
+/// then attempting to read the body will result in a response with status code
+/// 413: Entity too large will be returned to the client.
+///
+/// This limit only applies for files in a multipart body that get streamed to
+/// disc. For headers and other content that gets read into memory use the
+/// `max_files_size` limit.
+///
pub fn set_max_files_size(request: Request, size: Int) -> Request {
Connection(..request.body, max_files_size: size)
|> request.set_body(request, _)
}
-// TODO: document
+/// Get the maximum permitted total size of a files uploaded by a request in
+/// bytes.
+///
pub fn get_max_files_size(request: Request) -> Int {
request.body.max_files_size
}
-// TODO: document
+/// The the size limit for each chunk of the request body when read from the
+/// client.
+///
+/// This value is passed to the underlying web server when reading the body and
+/// the exact size of chunks read depends on the server implementation. It most
+/// likely will read chunks smaller than this size if not yet enough data has
+/// been received from the client.
+///
pub fn set_read_chunk_size(request: Request, size: Int) -> Request {
Connection(..request.body, read_chunk_size: size)
|> request.set_body(request, _)
}
-// TODO: document
+/// Get the size limit for each chunk of the request body when read from the
+/// client.
+///
pub fn get_read_chunk_size(request: Request) -> Int {
request.body.read_chunk_size
}
+/// A convenient alias for a HTTP request with a Wisp connection as the body.
+///
pub type Request =
HttpRequest(Connection)
-// TODO: document
+/// This middleware function ensures that the request has a specific HTTP
+/// method, returning an empty response with status code 405: Method not allowed
+/// if the method is not correct.
+///
+/// # Examples
+///
+/// ```gleam
+/// fn handle_request(request: Request) -> Response {
+/// use <- wisp.require_method(request, http.Patch)
+/// // ...
+/// }
+/// ```
+///
pub fn require_method(
request: HttpRequest(t),
method: Method,
@@ -321,6 +518,14 @@ pub const path_segments = request.path_segments
///
///
///
+/// # Examples
+///
+/// ```gleam
+/// fn handle_request(request: Request) -> Response {
+/// let request = wisp.method_override(request)
+/// // The method has now been overridden if appropriate
+/// }
+///
pub fn method_override(request: HttpRequest(a)) -> HttpRequest(a) {
use <- bool.guard(when: request.method != http.Post, return: request)
{
@@ -342,6 +547,23 @@ pub fn method_override(request: HttpRequest(a)) -> HttpRequest(a) {
// TODO: note you probably want a `require_` function
// TODO: note it'll hang if you call it twice
// TODO: note it respects the max body size
+/// A middleware function which reads the entire body of the request as a string.
+///
+/// If the body is larger than the `max_body_size` limit then an empty response
+/// with status code 413: Entity too large will be returned to the client.
+///
+/// If the body is found not to be valid UTF-8 then an empty response with
+/// status code 400: Bad request will be returned to the client.
+///
+/// # Examples
+///
+/// ```gleam
+/// fn handle_request(request: Request) -> Response {
+/// use body <- wisp.require_string_body(request)
+/// // ...
+/// }
+/// ```
+///
pub fn require_string_body(
request: Request,
next: fn(String) -> Response,
@@ -601,7 +823,24 @@ fn sort_keys(pairs: List(#(String, t))) -> List(#(String, t)) {
list.sort(pairs, fn(a, b) { string.compare(a.0, b.0) })
}
-// TODO: document
+// TODO: determine is this a good API. Perhaps the response should be
+// parameterised?
+/// A middleware function which returns an empty response with the status code
+/// 400: Bad request if the result is an error.
+///
+/// This function is similar to the `try` function of the `gleam/result` module,
+/// except returning a HTTP response rather than the error when the result is
+/// not OK.
+///
+/// # Example
+///
+/// ```gleam
+/// fn handle_request(request: Request) -> Response {
+/// use value <- wisp.request(result_returning_function())
+/// // ...
+/// }
+/// ```
+///
pub fn require(
result: Result(value, error),
next: fn(value) -> Response,
@@ -612,15 +851,27 @@ pub fn require(
}
}
+/// Data parsed from form sent in a request's body.
+///
pub type FormData {
FormData(
+ /// String values of the form's fields.
values: List(#(String, String)),
+ /// Uploaded files.
files: List(#(String, UploadedFile)),
)
}
pub type UploadedFile {
- UploadedFile(file_name: String, path: String)
+ UploadedFile(
+ /// The name that was given to the file in the form.
+ /// This is user input and should not be trusted.
+ file_name: String,
+ /// The location of the file on the server.
+ /// This is a temporary file and will be deleted when the request has
+ /// finished being handled.
+ path: String,
+ )
}
//
@@ -741,9 +992,20 @@ fn extension_to_mime_type(extension: String) -> String {
// Middleware
//
-// TODO: document
-pub fn rescue_crashes(service: fn() -> Response) -> Response {
- case erlang.rescue(service) {
+/// A middleware function that rescues crashes and returns an empty response
+/// with status code 500: Internal server error.
+///
+/// # Examples
+///
+/// ```gleam
+/// fn handle_request(req: Request) -> Response {
+/// use <- wisp.rescue_crashes
+/// // ...
+/// }
+/// ```
+///
+pub fn rescue_crashes(handler: fn() -> Response) -> Response {
+ case erlang.rescue(handler) {
Ok(response) -> response
Error(error) -> {
// TODO: log the error
@@ -753,11 +1015,23 @@ pub fn rescue_crashes(service: fn() -> Response) -> Response {
}
}
-// TODO: test
-// TODO: document
-// TODO: real implementation that uses the logger
-pub fn log_requests(req: Request, service: fn() -> Response) -> Response {
- let response = service()
+// TODO: test, somehow.
+/// A middleware function that logs details about the request and response.
+///
+/// The format used logged by this middleware may change in future versions of
+/// Wisp.
+///
+/// # Examples
+///
+/// ```gleam
+/// fn handle_request(req: Request) -> Response {
+/// use <- wisp.log_request(req)
+/// // ...
+/// }
+/// ```
+///
+pub fn log_request(req: Request, handler: fn() -> Response) -> Response {
+ let response = handler()
[
int.to_string(response.status),
" ",
@@ -766,6 +1040,7 @@ pub fn log_requests(req: Request, service: fn() -> Response) -> Response {
req.path,
]
|> string.concat
+ // TODO: use the logger
|> io.println
response
}
@@ -786,12 +1061,40 @@ fn join_path(a: String, b: String) -> String {
}
}
-// TODO: document
+/// A middleware function that serves files from a directory, along with a
+/// suitable `content-type` header for known file extensions.
+///
+/// Files are sent using the `File` response body type, so they will be sent
+/// directly to the client from the disc, without being read into memory.
+///
+/// The `under` parameter is the request path prefix that must match for the
+/// file to be served.
+///
+/// | `under` | `from` | `request.path` | `file` |
+/// |-----------|---------|--------------------|-------------------------|
+/// | `/static` | `/data` | `/static/file.txt` | `/data/file.txt` |
+/// | `` | `/data` | `/static/file.txt` | `/data/static/file.txt` |
+/// | `/static` | `` | `/static/file.txt` | `file.txt` |
+///
+/// This middleware will discard any `..` path segments in the request path to
+/// prevent the client from accessing files outside of the directory. It is
+/// advised not to serve a directory that contains your source code, application
+/// configuration, database, or other private files.
+///
+/// # Examples
+///
+/// ```gleam
+/// fn handle_request(req: Request) -> Response {
+/// use <- wisp.log_request(req)
+/// // ...
+/// }
+/// ```
+///
pub fn serve_static(
req: Request,
under prefix: String,
from directory: String,
- next service: fn() -> Response,
+ next handler: fn() -> Response,
) -> Response {
let path = remove_preceeding_slashes(req.path)
let prefix = remove_preceeding_slashes(prefix)
@@ -812,14 +1115,14 @@ pub fn serve_static(
// TODO: better check for file existence.
case file_info(path) {
- Error(_) -> service()
+ Error(_) -> handler()
Ok(_) ->
response.new(200)
|> response.set_header("content-type", mime_type)
|> response.set_body(File(path))
}
}
- _, _ -> service()
+ _, _ -> handler()
}
}
@@ -835,10 +1138,13 @@ fn make_directory(path: String) -> Result(Nil, simplifile.FileError) {
}
}
-// TODO: test
-// TODO: document
-// TODO: document that you need to call `remove_temporary_files` when you're
-// done, unless you're using `mist_service` which will do it for you.
+/// Create a new temporary directory for the given request.
+///
+/// If you are using the `mist_service` function or another compliant web server
+/// adapter then this file will be deleted for you when the request is complete.
+/// Otherwise you will need to call the `delete_temporary_files` function
+/// yourself.
+///
pub fn new_temporary_file(
request: Request,
) -> Result(String, simplifile.FileError) {
@@ -854,8 +1160,12 @@ pub fn new_temporary_file(
@external(erlang, "file", "del_dir_r")
fn del_dir(path: String) -> Dynamic
-// TODO: test
-// TODO: document
+/// Delete any temporary files created for the given request.
+///
+/// If you are using the `mist_service` function or another compliant web server
+/// adapter then this file will be deleted for you when the request is complete.
+/// Otherwise you will need to call this function yourself.
+///
pub fn delete_temporary_files(
request: Request,
) -> Result(Nil, simplifile.FileError) {
@@ -874,10 +1184,8 @@ fn file_info(path: String) -> Result(Dynamic, Dynamic)
// Cryptography
//
-pub const strong_random_bytes = crypto.strong_random_bytes
-
fn random_slug() -> String {
- strong_random_bytes(16)
+ crypto.strong_random_bytes(16)
|> base.url_encode64(False)
}
@@ -904,7 +1212,7 @@ pub fn test_request(body: BitString) -> Request {
// TODO: test
// TODO: document
-pub fn body_to_string_builder(body: ResponseBody) -> StringBuilder {
+pub fn body_to_string_builder(body: Body) -> StringBuilder {
case body {
Empty -> string_builder.new()
Text(text) -> text
@@ -917,7 +1225,7 @@ pub fn body_to_string_builder(body: ResponseBody) -> StringBuilder {
// TODO: test
// TODO: document
-pub fn body_to_bit_builder(body: ResponseBody) -> BitBuilder {
+pub fn body_to_bit_builder(body: Body) -> BitBuilder {
case body {
Empty -> bit_builder.new()
Text(text) -> bit_builder.from_string_builder(text)