Sign in

    pony

    web
    framework
    auth
    Download zip
    Author
    Version
    0.1.1
    License
    Apache-2.0
    Last updated
    4 months ago
    Downloads
    83

    #jaredzhou/pony

    A lightweight HTTP web framework for MoonBit, inspired by Go's Chi.

    #Installation

    Add to your moon.mod:

    { "deps": { "jaredzhou/pony": "0.1.0" } }

    #Quick Start

    ///|
    fn main {
    let r = Router::Router()

    // GET /ping => 200 "pong"
    r.add(HttpMethod::Get, "/ping", fn(ctx) { ctx.write_text(status_ok, "pong") })

    // GET /users/{id} => 200 "user: 42"
    r.add(HttpMethod::Get, "/users/{id}", fn(ctx) {
    let id = ctx.param("id").or("")
    ctx.write_text(status_ok, "user: \(id)")
    })

    // GET /search?q=pony => 200 "searching: pony"
    r.add(HttpMethod::Get, "/search", fn(ctx) {
    let q = ctx.query("q").or("")
    ctx.write_text(status_ok, "searching: \(q)")
    })

    // GET /api/status => 200 {"status":"ok"}
    r.add(HttpMethod::Get, "/api/status", fn(ctx) {
    ctx.response_ok({ "status": "ok" })
    })

    // POST /api/items => 400 {"code":3,"message":"name is required"}
    r.add(HttpMethod::Post, "/api/items", fn(ctx) {
    ctx.response_error(invalid_argument, "name is required")
    })

    // GET /static/css/app.css => 200 "file: css/app.css"
    r.add(HttpMethod::Get, "/static/*", fn(ctx) {
    let path = ctx.wildcard().or("")
    ctx.write_text(status_ok, "file: \(path)")
    })

    start("127.0.0.1:3000", r)
    }

    #Features

    #Router

    • Radix tree with priority matching for static, parameter, and wildcard routes
    • {param} path parameters and * wildcard capture
    • Route-specific middleware per endpoint
    • Sub-router mounting with mount()
    • Custom 404 and 405 handlers

    #Middleware

    Built-in via jaredzhou/pony/mw:

    r.use_mw(@mw.logger())
    r.use_mw(@mw.cors(
    allow_origins=["*"],
    allow_methods=["GET", "POST"],
    ))
    r.use_mw(@mw.jwt(new_hmac_sha256(secret)))

    MiddlewareDescription
    logger()Request logging (method, path, status, duration)
    cors()CORS headers with configurable origins, methods, headers, max-age
    jwt(signing_method)JWT bearer token auth, stores sub claim in context

    #Request Context (Context)

    let id = ctx.param("id") // path parameter
    let q = ctx.query("search") // query string
    let header = ctx.header("Accept") // request header
    let body = ctx.json()? // auto-deserialize JSON body
    let form = ctx.form("name")? // form values (application/x-www-form-urlencoded)

    ctx.set_content_type("application/json")
    ctx.write_text(200, "Hello")
    ctx.write_json(200, {"key": "value"})
    ctx.response_ok({"status": "ok"}) // 200 JSON
    ctx.response_error(400, "bad input") // error JSON
    ctx.redirect("/login") // 302 redirect
    ctx.no_content() // 204

    #Extension Store

    Type-safe key-value store for request-scoped data:

    ctx.set_ext(@pony.RequestId{}, "req-abc")
    ctx.set_ext(@pony.UserId{}, "user-42")

    let uid = ctx.get_ext(@pony.UserId{})

    Full HTTP header management with canonical keys and multi-value support:

    let h = @pony.new()
    h.set("Content-Type", "application/json")
    h.add("Set-Cookie", "session=abc")
    let ct = h.get("Content-Type") // "application/json"

    #Server

    let server = @pony.Server::new("127.0.0.1:3000", router)
    .with_timeout(read=5000, write=10000)
    server.start()!

    #License

    Apache-2.0

    ExtError

    pub(all) suberror ExtError {
    Missing
    DecodeError(String, String)
    } derive(Eq, ToJson,
    Debug
    )

    RouterError

    pub suberror RouterError {
    InvalidPattern(String)
    } derive(ToJson,
    Debug
    )

    ApiError

    ApiError::new

    fn ApiError::new(code : Int, message : String) -> ApiError

    ApiError::to_http_status

    fn ApiError::to_http_status(code : Int) -> Int

    Context

    pub(all) struct Context {
    http_method : HttpMethod
    url_str : String
    url :
    URL

    route_path : String
    req_headers : Map[String, String]
    resp_headers : Map[String, String]
    queries : Values
    form_values : Values?
    exts : ExtStore
    path_params : Array[ParamKV]
    req_body : &
    Reader

    resp_writer :
    ServerConnection

    }

    impl Show for Context

    Context::form

    async fn Context::form(self : Context, key : String) -> String?

    Context::get_ext

    fn[K, V] Context::get_ext(self : Context, _marker : K) -> V raise ExtError

    Context::header

    fn Context::header(self : Context, key : String) -> String?

    Context::json

    Context::no_content

    async fn Context::no_content(self : Context) -> Unit

    Context::param

    fn Context::param(self : Context, name : String) -> String?

    Context::query

    fn Context::query(self : Context, key : String) -> String?

    Context::redirect

    async fn Context::redirect(self : Context, url : String, http_status? : Int) -> Unit

    Context::remove_ext

    fn[K] Context::remove_ext(self : Context, _marker : K) -> Unit

    Context::response_error

    async fn Context::response_error(self : Context, code : Int, msg : String) -> Unit

    Context::response_ok

    async fn[T : ToJson] Context::response_ok(self : Context, value : T) -> Unit

    Context::set_content_type

    fn Context::set_content_type(self : Context, ct : String) -> Unit

    Context::set_ext

    fn[K, V] Context::set_ext(self : Context, _marker : K, v : V) -> Unit

    Context::set_header

    fn Context::set_header(self : Context, key : String, value : String) -> Unit

    Context::try_get_ext

    fn[K, V] Context::try_get_ext(self : Context, _marker : K) -> V?

    Context::wildcard

    fn Context::wildcard(self : Context) -> String?

    Context::write_bytes

    async fn Context::write_bytes(self : Context, http_status : Int, body : Bytes) -> Unit

    Context::write_json

    async fn[T : ToJson] Context::write_json(self : Context, http_status : Int, msg? : String, v : T) -> Unit

    Context::write_status

    async fn Context::write_status(self : Context, http_status : Int, end_response? : Bool) -> Unit

    Context::write_text

    async fn Context::write_text(self : Context, http_status : Int, msg? : String, text : String) -> Unit

    Endpoint

    ExtStore

    pub(all) struct ExtStore(Array[(String,
    Any
    )])

    ExtStore::get

    fn[K, V] ExtStore::get(self : ExtStore, _marker : K) -> V raise ExtError

    ExtStore::new

    fn ExtStore::new() -> ExtStore

    ExtStore::remove

    fn[K] ExtStore::remove(self : ExtStore, _marker : K) -> Unit

    ExtStore::set

    fn[K, V] ExtStore::set(self : ExtStore, _marker : K, v : V) -> Unit

    ExtStore::try_get

    fn[K, V] ExtStore::try_get(self : ExtStore, _marker : K) -> V?

    Handler

    pub(all) struct Handler(async (Context) -> Unit) derive(
    Debug
    )

    pub struct Header(Map[String, Array[String]])

    Header::add

    fn Header::add(h : Header, key : String, value : String) -> Unit

    Header::copy

    fn Header::copy(h : Header) -> Header

    Header::del

    fn Header::del(h : Header, key : String) -> Unit

    Header::get

    fn Header::get(h : Header, key : String) -> String?

    Header::has

    fn Header::has(h : Header, key : String) -> Bool

    Header::set

    fn Header::set(h : Header, key : String, value : String) -> Unit

    Header::values

    fn Header::values(h : Header, key : String) -> Array[String]

    HttpMethod

    pub(all) enum HttpMethod {
    Get
    Head
    Post
    Put
    Delete
    Connect
    Options
    Trace
    Patch
    Any
    } derive(Compare, Eq, Hash, ToJson,
    Debug
    )

    impl Show for HttpMethod

    Middleware

    pub(all) struct Middleware((Handler) -> Handler) derive(
    Debug
    )

    Node

    impl Show for Node

    NodeType

    impl Show for NodeType

    ParamKV

    type ParamKV

    RequestId

    pub(all) struct RequestId {
    }

    Marker type for per-request extension data (e.g. request ID).

    RouteResult

    pub enum RouteResult {
    Found(Endpoint, Array[String])
    MethodNotAllowed
    NotFound
    }

    Router

    pub struct Router {
    tree : Node
    middlewares : Array[Middleware]
    not_found_handler : Handler?
    method_not_allowed_handler : Handler?
    } derive(
    Debug
    )
    fn Router::Router() -> Router

    impl Show for Router

    Router::add

    fn Router::add(self : Router, meth : HttpMethod, pattern : String, name? : String, mws? : Array[Middleware], handler : Handler) -> Unit raise

    Add a new route to the router

    Parameters

    • meth: HTTP method
    • pattern: URL pattern, must start with /
    • mws: Optional array of middlewares, applied in order before the handler only applies to this route
    • name: Optional name for the route
    • handler: Handler function

    Raises

    • RouterError::InvalidPattern: If the pattern does not start with / or is otherwise invalid

    Router::any

    fn Router::any(self : Router, pattern : String, name? : String, mws? : Array[Middleware], handler : Handler) -> Unit raise

    Router::handler

    async fn Router::handler(self : Router, ctx : Context) -> Unit

    Router::mount

    fn Router::mount(self : Router, prefix : String, sub : Router) -> Unit raise

    Mount a sub-router under the given prefix using a prefix/* wildcard route, like chi does. The sub-router stays alive — routes added to it after mount are automatically picked up.

    Router::set_method_not_allowed

    fn Router::set_method_not_allowed(self : Router, handler : Handler) -> Unit

    Router::set_not_found

    fn Router::set_not_found(self : Router, handler : Handler) -> Unit

    Router::use_mw

    fn Router::use_mw(self : Router, mw : Middleware) -> Unit

    Server

    pub struct Server {
    router : Router
    addr : String
    read_timeout : Int?
    write_timeout : Int?
    request_id : Int
    }
    fn Server::Server(addr : String, router : Router) -> Server

    Server::start

    async fn Server::start(self : Server) -> Unit

    Server::with_timeout

    fn Server::with_timeout(self : Server, read? : Int, write? : Int) -> Server

    UserId

    pub(all) struct UserId {
    }

    Marker type for per-user extension data (e.g. authenticated user ID).

    Values

    pub(all) struct Values(Map[String, Array[String]])

    Values::get

    fn Values::get(self : Values, key : String) -> String?

    Values::get_all

    fn Values::get_all(self : Values, key : String) -> Array[String]

    Values::new

    fn Values::new() -> Values

    aborted

    let aborted : Int

    already_exists

    let already_exists : Int

    cancelled

    let cancelled : Int

    canonicalHeaderKey

    fn canonicalHeaderKey(key : String) -> String

    data_loss

    let data_loss : Int

    deadline_exceeded

    let deadline_exceeded : Int

    failed_precondition

    let failed_precondition : Int

    internal

    let internal : Int

    invalid_argument

    let invalid_argument : Int

    new

    fn new() -> Header

    not_found

    let not_found : Int

    let ok : Int

    out_of_range

    let out_of_range : Int

    permission_denied

    let permission_denied : Int

    register

    fn register(code : Int, http_status : Int) -> Unit

    resource_exhausted

    let resource_exhausted : Int

    start

    async fn start(addr : String, router : Router) -> Unit

    status_accepted

    let status_accepted : Int

    status_already_reported

    let status_already_reported : Int

    status_bad_gateway

    let status_bad_gateway : Int

    status_bad_request

    let status_bad_request : Int

    status_conflict

    let status_conflict : Int

    status_continue

    let status_continue : Int

    status_created

    let status_created : Int

    status_early_hints

    let status_early_hints : Int

    status_expectation_failed

    let status_expectation_failed : Int

    status_failed_dependency

    let status_failed_dependency : Int

    status_forbidden

    let status_forbidden : Int

    status_found

    let status_found : Int

    status_gateway_timeout

    let status_gateway_timeout : Int

    status_gone

    let status_gone : Int

    status_http_version_not_supported

    let status_http_version_not_supported : Int

    status_im_used

    let status_im_used : Int

    status_insufficient_storage

    let status_insufficient_storage : Int

    status_internal_server_error

    let status_internal_server_error : Int

    status_length_required

    let status_length_required : Int

    status_locked

    let status_locked : Int

    status_loop_detected

    let status_loop_detected : Int

    status_method_not_allowed

    let status_method_not_allowed : Int

    status_misdirected_request

    let status_misdirected_request : Int

    status_moved_permanently

    let status_moved_permanently : Int

    status_multi_status

    let status_multi_status : Int

    status_multiple_choices

    let status_multiple_choices : Int

    status_network_authentication_required

    let status_network_authentication_required : Int

    status_no_content

    let status_no_content : Int

    status_non_authoritative_info

    let status_non_authoritative_info : Int

    status_not_acceptable

    let status_not_acceptable : Int

    status_not_extended

    let status_not_extended : Int

    status_not_found

    let status_not_found : Int

    status_not_implemented

    let status_not_implemented : Int

    status_not_modified

    let status_not_modified : Int

    status_ok

    let status_ok : Int

    status_partial_content

    let status_partial_content : Int

    status_payment_required

    let status_payment_required : Int

    status_permanent_redirect

    let status_permanent_redirect : Int

    status_precondition_failed

    let status_precondition_failed : Int

    status_precondition_required

    let status_precondition_required : Int

    status_processing

    let status_processing : Int

    status_proxy_auth_required

    let status_proxy_auth_required : Int

    status_request_entity_too_large

    let status_request_entity_too_large : Int

    status_request_header_fields_too_large

    let status_request_header_fields_too_large : Int

    status_request_timeout

    let status_request_timeout : Int

    status_request_uri_too_long

    let status_request_uri_too_long : Int

    status_requested_range_not_satisfiable

    let status_requested_range_not_satisfiable : Int

    status_reset_content

    let status_reset_content : Int

    status_see_other

    let status_see_other : Int

    status_service_unavailable

    let status_service_unavailable : Int

    status_switching_protocols

    let status_switching_protocols : Int

    status_temporary_redirect

    let status_temporary_redirect : Int

    status_text

    fn status_text(code : Int) -> String

    status_too_early

    let status_too_early : Int

    status_too_many_requests

    let status_too_many_requests : Int

    status_unauthorized

    let status_unauthorized : Int

    let status_unavailable_for_legal_reasons : Int

    status_unprocessable_entity

    let status_unprocessable_entity : Int

    status_unsupported_media_type

    let status_unsupported_media_type : Int

    status_upgrade_required

    let status_upgrade_required : Int

    status_use_proxy

    let status_use_proxy : Int

    status_variant_also_negotiates

    let status_variant_also_negotiates : Int

    unauthenticated

    let unauthenticated : Int

    unavailable

    let unavailable : Int

    unimplemented

    let unimplemented : Int

    unknown

    let unknown : Int