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)
payload = Bonsai.read(stream, Body(x=Int, y=Float64, z=String))
# typeof(payload)
# NamedTuple{(:x, :y, :z), Tuple{Int64, Float64, String}}
Bonsai.write(stream, Body(payload))
endBonsai.write will attempt to set the correct content-type header for the data, however, this can be changed by defining mime_type.
Bonsai.mime_type(::Type{MyType}) = "text/plain"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
Bonsai.write(stream, Body("Created"), Status(201))
endRouting
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 variableidthat 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 variableidthat 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
app.post["/pet/{id}"]You can extract the route parameters using the Route type, which will return a named tuple.
function get_by_id(stream)
(;id) = Bonsai.read(stream, Route(id=String))
end
app.get["/{id}"] = get_by_idThe 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)
(;color) = Bonsai.read(
stream,
#/<some route>?color=blue
Query(color = Union{Nothing, String}),
)
endNote 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)
headers = Bonsai.read(stream, Headers(content_type=String))
if headers.content_type == "application/json"
Bonsai.write(
stream,
Body(json_data),
Headers(content_type="application/json")
)
else
Bonsai.write(
stream,
Body(csv_data),
Headers(content_type="text/csv")
)
end
endFor Headers the keys will be transformed using Bonsai.headerize and the matching is case-insensitive.
Bonsai.headerize(:content_type)
# "content-type"Files
Writing files supports AbstractPaths defined in FilePaths. The content type will be set based on the file extension.
file = Path("data/some-file.json")
Bonsai.write(stream, Body(file))A nice feature of this is we can easily use other AbstractPath implementations for example like that in AWSS3
file = S3Path("s3://my.bucket/test1.txt")
Bonsai.write(stream, Body(file))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
id::UUID = uuid4()
created::Date = now()
updated::Date = now()
end
"""
Markdown documentation included in OpenAPI description
* name - first and last name
"""
@data struct Person
BaseModel...
name::String
endThis expands to roughly the following source code
Base.@kwdef struct Person
# Note splatted fields from BaseModel
id::UUID = uuid4()
created::Date = now()
updated::Date = now()
name::String
end
# Allows struct to be correctly read from JSON
StructTypes.StructType(::Type{Person}) = StructTypes.Struct()
# Set the correct description to be used in OpenAPI docs
Bonsai.description(t::Type{Person}) = Bonsai.docstr(t)To help with generating boilerplate you can use JSON3.generatedtypes.