Handlers
Each handler is a function with the signature f(stream::HTTP.Stream)
. You read and write from the HTTP stream using Bonsai.read
or Bonsai.write
along with a wrapper type (Body
, Query
, Headers
, Route
and Status
) to specify the location. The data type being read/written should be an AbstractDict
, NamedTuple
or have a StructType defined. Both read
and write
support variadic arguments e.g fn(stream, args...)
Body
Below is an example of reading JSON from a request, and then writing it back as a response
function get_index(stream)
= Bonsai.read(stream, Body(x=Int, y=Float64, z=String))
payload # typeof(payload)
# NamedTuple{(:x, :y, :z), Tuple{Int64, Float64, String}}
write(stream, Body(payload))
Bonsai.end
Bonsai.write
will attempt to set the correct content-type header for the data, however, this can be changed by defining mime_type
.
mime_type(::Type{MyType}) = "text/plain" Bonsai.
The content type is defined for the following types already
Union{NamedTuple, AbstractDict}
- application/jsonAbstractString
- text/plain- AbstractPath - Based on the file extension.
Status Codes
The default status code is 200, however other status codes can be returned by using Status
function post_index(stream)
#...first part of handler
write(stream, Body("Created"), Status(201))
Bonsai.end
Routing
Routing relies on the router from HTTP.jl, as such the functionality is the same (see the copied doc stings below).
The following path types are allowed for matching:
/api/widgets
- exact match of static strings/api/*/owner
- single*
to wildcard match any string for a single segment/api/widget/{id}
- Define a path variableid
that matches any value provided for this segment; path variables are available in the request context likereq.context[:params]["id"]
/api/widget/{id:[0-9]+}
- Define a path variableid
that only matches integers for this segment/api/**
- double wildcard matches any number of trailing segments in the request path; must be the last segment in the path
To register a route use the dot
syntax to specify the HTTP Method and then dictionary indexing for the requested route. e.g
"/pet/{id}"] app.post[
You can extract the route parameters using the Route
type, which will return a named tuple.
function get_by_id(stream)
= Bonsai.read(stream, Route(id=String))
(;id) end
"/{id}"] = get_by_id app.get[
The Route
constructor takes the parameter name and the type. Multiple keyword arguments are supported
Query
Following a similar pattern as the others, query parameters can be matched by using Query
type.
function get_car(stream)
= Bonsai.read(
(;color)
stream, #/<some route>?color=blue
Query(color = Union{Nothing, String}),
)end
Note to handle optional parameters you can use a union with nothing e.g Union{Nothing, T}
.
Headers
Use Headers
to read and write specific headers. For example, a handler that can return JSON or a CSV depending on the content type header.
function index(stream)
= Bonsai.read(stream, Headers(content_type=String))
headers if headers.content_type == "application/json"
write(
Bonsai.
stream, Body(json_data),
Headers(content_type="application/json")
)else
write(
Bonsai.
stream, Body(csv_data),
Headers(content_type="text/csv")
)end
end
For Headers
the keys will be transformed using Bonsai.headerize
and the matching is case-insensitive.
headerize(:content_type)
Bonsai.# "content-type"
Files
Writing files supports AbstractPaths
defined in FilePaths. The content type will be set based on the file extension.
= Path("data/some-file.json")
file write(stream, Body(file)) Bonsai.
A nice feature of this is we can easily use other AbstractPath
implementations for example like that in AWSS3
= S3Path("s3://my.bucket/test1.txt")
file write(stream, Body(file)) Bonsai.
JSON
Because working with JSON is so common there is a @data
macro that can help you define structs for it. This macro supports Base.@kwdef
and @composite, for example.
using Dates, UUIDs, Bonsai, StructTypes
using Bonsai: @data
@data struct BaseModel
::UUID = uuid4()
id::Date = now()
created::Date = now()
updatedend
"""
Markdown documentation included in OpenAPI description
* name - first and last name
"""
@data struct Person
...
BaseModel::String
nameend
This expands to roughly the following source code
Base.@kwdef struct Person
# Note splatted fields from BaseModel
::UUID = uuid4()
id::Date = now()
created::Date = now()
updated::String
nameend
# Allows struct to be correctly read from JSON
StructType(::Type{Person}) = StructTypes.Struct()
StructTypes.# Set the correct description to be used in OpenAPI docs
description(t::Type{Person}) = Bonsai.docstr(t) Bonsai.
To help with generating boilerplate you can use JSON3.generatedtypes
.