tyroute

    Typed request extraction, responses, and OpenAPI export for MoonBack.

    Download zip
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    5 hours ago
    Downloads
    4

    #tyroute

    Typed request extraction, responses, and OpenAPI 3.2 export for MoonBack.

    The module is moonbit-community/tyroute; its root package is @tyroute. It uses MoonBack 0.8.9 and moonbitlang/async 0.22.5. The default target is wasm; the HTTP example supports both wasm and native.

    #Use with MoonBack

    In your application's moon.pkg, import:

    import {
    "moonbit-community/tyroute",
    "moonbitlang/moonback",
    "moonbitlang/async",
    "moonbitlang/core/json",
    }

    options("is-main": true)

    The corresponding modules must also be dependencies in your moon.mod. Set options(preferred_target: "wasm") there.

    Put the following code in the application's main.mbt:

    ///|
    #derive(FromJson, ToJson)
    struct User {
    id : Int32
    name : String
    }

    ///|
    #derive(FromJson)
    struct UserQuery {
    name : String?
    }

    ///|
    fn get_user(
    id : Int32,
    query : @tyroute.ViaJson[UserQuery],
    _headers : Unit,
    ) -> @tyroute.ViaJson[User] {
    let name = query.0.name.unwrap_or("MoonBit")
    @tyroute.ViaJson({ id, name, })
    }

    ///|
    fn create_user(
    _params : Unit,
    _query : Unit,
    _headers : Unit,
    body : @tyroute.ViaJson[User],
    ) -> (@tyroute.StatusCode, @tyroute.ViaJson[User]) {
    (@tyroute.StatusCode(201), body)
    }

    ///|
    async fn serve() -> Unit {
    let router = @tyroute.Router()
    ..openapi("/openapi.json", title="Users API", version="1.0.0")
    ..get("/users/:id", get_user)
    ..post("/users", create_user)
    let app = @moonback.App(ctx => {
    ctx.use_(router)
    })
    defer app.close()
    let server = @moonback.listen(host="127.0.0.1", port=3000)
    println("Listening on http://127.0.0.1:3000")
    app.serve(server)
    }

    The handlers and serve function above are compiled as part of this README. The main entry point belongs to the application package.

    Router implements MoonBack's Extension trait. ctx.use_(router) registers its routes in the app, where they can coexist with MoonBack routes.

    Run the bundled example from the tyroute module directory:

    moon run example

    Its source is in example/main.mbt. For a separate application, run moon run . in its directory.

    Then try:

    curl 'http://127.0.0.1:3000/users/42?name=MoonBit' curl -i -X POST http://127.0.0.1:3000/users \ -H 'Content-Type: application/json' \ -d '{"id":42,"name":"MoonBit"}' curl http://127.0.0.1:3000/openapi.json

    The GET returns 200 with {"id":42,"name":"MoonBit"}. The POST decodes the JSON body into User and returns it with status 201.

    #Handler arguments and responses

    GET, HEAD, and OPTIONS handlers take path parameters, query parameters, and headers, in that order. POST, PUT, PATCH, and DELETE handlers also take a fourth argument for the body. Use Unit for an input the handler does not need.

    Path captures use MoonBack's :name syntax. A tuple such as (String, Int) extracts two captures in path order. Query and header maps use FromString to parse their values. ViaJson[T] decodes JSON bodies through FromJson and encodes JSON responses through ToJson.

    Custom types can implement FromParam, FromQuery, FromHeaders, FromBody, and ToResponse. Each trait has a default unconstrained schema() that custom types can override.

    #OpenAPI

    router.generate_openapi(title="My API", version="1.0.0") returns an OpenAPI 3.2 JSON document without calling the handlers. It preserves whole-query schemas, including combinations such as anyOf, through an in: querystring parameter.

    router.openapi("/openapi.json", title="My API", version="1.0.0") configures an OpenAPI endpoint. When ctx.use_(router) initializes the router, it generates the document once and registers a GET route serving it with status 200 and content type application/json. The document includes all API routes added before initialization, including routes added after .openapi(...). Schema generation errors are reported during initialization.

    Both methods accept components=... when custom schemas reference local components.

    FromJson and ToJson do not provide field schemas, so ViaJson[T] uses the unconstrained default. The current export uses default responses and */* media types because the schema interface does not declare status codes or media types.

    #Development

    Run these commands from this directory. README.md links to this file, and the MoonBit example is checked with the package.

    moon check moon test moon fmt moon info

    FromBody

    pub(open) trait FromBody {
    fn schema() ->
    Json
    = _
    async fn from_body(
    RequestBody
    ) -> Self
    }

    FromHeaders

    pub(open) trait FromHeaders {
    fn schema() ->
    Json
    = _
    fn from_headers(
    Headers
    ) -> Self raise HandlerError
    }

    impl FromHeaders for Map[String, T]

    FromParam

    pub(open) trait FromParam {
    fn schema() ->
    Json
    = _
    fn from_param(Array[(StringView, StringView)]) -> Self raise HandlerError
    }

    Parameters are consumed from the end of the array, including tuple elements.
    impl FromParam for Unit
    impl FromParam for Bool
    impl FromParam for Char
    impl FromParam for Int
    impl FromParam for Int64
    impl FromParam for UInt
    impl FromParam for UInt64
    impl FromParam for Float
    impl FromParam for Double
    impl FromParam for String
    impl FromParam for Tuple2[A, B]
    impl FromParam for Tuple3[A, B, C]
    impl FromParam for Tuple4[A, B, C, D]
    impl FromParam for Tuple5[A, B, C, D, E]
    impl FromParam for Tuple6[A, B, C, D, E, F]

    FromQuery

    pub(open) trait FromQuery {
    fn schema() ->
    Json
    = _
    fn from_query(
    QueryParams
    ) -> Self raise HandlerError
    }

    impl FromQuery for Map[String, T]

    FromString

    pub(open) trait FromString {
    fn schema() ->
    Json
    = _
    fn from_string(StringView) -> Self raise HandlerError
    }

    impl FromString for Bool
    impl FromString for Char
    impl FromString for Int
    impl FromString for Int64
    impl FromString for UInt
    impl FromString for Float

    ToResponse

    pub(open) trait ToResponse {
    fn schema() ->
    Json
    = _
    fn to_response(Self) -> Response raise HandlerError
    }

    impl ToResponse for Unit
    impl ToResponse for Bool
    impl ToResponse for Byte
    impl ToResponse for Char
    impl ToResponse for Int
    impl ToResponse for Int16
    impl ToResponse for Int64
    impl ToResponse for UInt
    impl ToResponse for Float
    impl ToResponse for Bytes

    HandlerError

    pub(all) suberror HandlerError {
    StringMismatch(String)
    ParamMismatch(String)
    QueryMismatch(String)
    HeaderMismatch(String)
    BodyMismatch(String)
    ResponseMismatch(String)
    }

    HandlerError::output

    fn HandlerError::output(self : HandlerError, logger : &Logger) -> Unit

    HandlerError::to_string

    fn HandlerError::to_string(self : HandlerError) -> String

    OpenApiError

    pub suberror OpenApiError {
    InvalidPath(String)
    InvalidSchema(String)
    DuplicateOperation(String)
    }

    Headers

    pub(all) struct Headers[T](T)

    Request

    type Request[Param, Query, Headers, Body]

    Response

    type Response

    Response::Response

    fn[A :
    ToHeaders
    ] Response::Response(status~ : Int, headers~ : A, body? : &
    Data
    ) -> Response

    Response::to_response

    fn Response::to_response(self : Response) -> Response raise HandlerError

    Router

    type Router

    impl Extension for Router

    Router::Router

    fn Router::Router() -> Router

    Router::add

    fn[P : FromParam, Q : FromQuery, H : FromHeaders, B : FromBody, R : ToResponse] Router::add(self : Router, path : String, meth :
    RequestMethod
    , handler : (P, Q, H, B) -> R) -> Unit

    Router::delete

    fn[P : FromParam, Q : FromQuery, H : FromHeaders, B : FromBody, R : ToResponse] Router::delete(self : Router, path : String, handler : (P, Q, H, B) -> R) -> Unit

    Router::generate_openapi

    fn Router::generate_openapi(self : Router, title~ : String, version~ : String, components? :
    Json
    ) ->
    Json
    raise OpenApiError

    Export the registered routes as an OpenAPI 3.2 document without invoking handlers. Schemas may be objects or booleans; {} remains unconstrained. Query constraints are preserved through an in: querystring parameter. Aggregate path/header constraints are retained in specification extensions. Status codes and media types are unknown to schema(), so responses use default and content uses /. Pass components when schemas use local $refs.

    Router::get

    fn[P : FromParam, Q : FromQuery, H : FromHeaders, R : ToResponse] Router::get(self : Router, path : String, handler : (P, Q, H) -> R) -> Unit

    Router::head

    fn[P : FromParam, Q : FromQuery, H : FromHeaders, R : ToResponse] Router::head(self : Router, path : String, handler : (P, Q, H) -> R) -> Unit

    Router::initialize

    async fn Router::initialize(self : Router, ctx :
    ModuleContext
    ) -> Unit

    Router::openapi

    fn Router::openapi(self : Router, url : String, title~ : String, version~ : String, components? :
    Json
    ) -> Unit

    Configure an OpenAPI endpoint. MoonBack initialization generates the document and registers a GET route serving it as application/json.

    Router::options

    fn[P : FromParam, Q : FromQuery, H : FromHeaders, R : ToResponse] Router::options(self : Router, path : String, handler : (P, Q, H) -> R) -> Unit

    Router::patch

    fn[P : FromParam, Q : FromQuery, H : FromHeaders, B : FromBody, R : ToResponse] Router::patch(self : Router, path : String, handler : (P, Q, H, B) -> R) -> Unit

    Router::post

    fn[P : FromParam, Q : FromQuery, H : FromHeaders, B : FromBody, R : ToResponse] Router::post(self : Router, path : String, handler : (P, Q, H, B) -> R) -> Unit

    Router::put

    fn[P : FromParam, Q : FromQuery, H : FromHeaders, B : FromBody, R : ToResponse] Router::put(self : Router, path : String, handler : (P, Q, H, B) -> R) -> Unit

    StatusCode

    pub(all) struct StatusCode(Int)