/// mbit full-feature demo — tests ALL framework capabilities.
/// Run: moon run demo --target native

///| ——— Data types for binding ———

struct User { name : String; age : Int } derive(@json.FromJson)
struct LoginReq { username : String; password : String } derive(@json.FromJson)

///| ——— Root ———

async fn h_root(ctx : @core.Context) -> Unit {
  ctx.json(200, Json::object(@core.h([("service",Json::string("mbit-demo")),("version",Json::string("0.1.0"))])))
}

///| ——— Health / Hello ———

async fn h_health(ctx : @core.Context) -> Unit {
  ctx.json(200, Json::object(@core.h([("status",Json::string("ok"))])))
}
async fn h_hello(ctx : @core.Context) -> Unit { ctx.string(200, "Hello, mbit!") }
async fn h_hello_name(ctx : @core.Context) -> Unit {
  ctx.string(200, "Hello, " + ctx.param_default("name","world") + "!")
}

///| ——— Query ———

async fn h_search(ctx : @core.Context) -> Unit {
  ctx.json(200, Json::object(@core.h([("q",Json::string(ctx.query_default("q",""))),("page",Json::string(ctx.query_default("page","1")))])))
}
async fn h_query_all(ctx : @core.Context) -> Unit {
  let map = ctx.query_map()
  let items : Array[(String,Json)] = []
  for k, v in map { items.push((k, Json::string(v))) }
  ctx.json(200, Json::object(@core.h(items)))
}

///| ——— Echo ———

async fn h_echo(ctx : @core.Context) -> Unit {
  match ctx.body_json() {
    Some(j) => ctx.json(200, j)
    None => ctx.json(400, Json::object(@core.h([("error",Json::string("bad JSON"))])))
  }
}

///| ——— CRUD ———

async fn h_create_user(ctx : @core.Context) -> Unit {
  let u : User? = @core.bind_json(ctx)
  match u {
    Some(user) => ctx.json(201, Json::object(@core.h([("msg",Json::string("Created")),("name",Json::string(user.name))])))
    None => ctx.json(400, Json::object(@core.h([("error",Json::string("bad user"))])))
  }
}
async fn h_put_user(ctx : @core.Context) -> Unit {
  let id = ctx.param_default("id","0")
  match ctx.body_json() {
    Some(j) => ctx.json(200, Json::object(@core.h([("msg",Json::string("Updated "+id)),("data",j)])))
    None => ctx.json(400, Json::object(@core.h([("error",Json::string("bad JSON"))])))
  }
}
async fn h_delete_user(ctx : @core.Context) -> Unit {
  ctx.json(200, Json::object(@core.h([("msg",Json::string("Deleted "+ctx.param_default("id","0")))])))
}
async fn h_patch_user(ctx : @core.Context) -> Unit {
  let id = ctx.param_default("id","0")
  match ctx.body_json() {
    Some(j) => ctx.json(200, Json::object(@core.h([("msg",Json::string("Patched "+id)),("data",j)])))
    None => ctx.json(400, Json::object(@core.h([("error",Json::string("bad JSON"))])))
  }
}

///| ——— Login ———

async fn h_login(ctx : @core.Context) -> Unit {
  let r : LoginReq? = @core.bind_json(ctx)
  match r {
    Some(req) => if req.username=="admin" && req.password=="secret" {
      ctx.json(200, Json::object(@core.h([("token",Json::string("demo-token")),("user",Json::string(req.username))])))
    } else { ctx.json(401, Json::object(@core.h([("error",Json::string("bad credentials"))]))) }
    None => ctx.json(400, Json::object(@core.h([("error",Json::string("bad data"))])))
  }
}

///| ——— Validation ———

async fn h_register(ctx : @core.Context) -> Unit {
  match ctx.body_json() {
    Some(json) => {
      let vals : Map[String,String] = Map([])
      match json { Json::Object(m) => for k,v in m { match v { Json::String(s)=>vals.set(k,s); Json::Number(n)=>vals.set(k,n.to_string()); _=>() } }; _ => () }
      let rules = [
        @core.FieldRules::new("username",[@core.Required,@core.Min(3),@core.Max(20)]),
        @core.FieldRules::new("password",[@core.Required,@core.Min(6)]),
        @core.FieldRules::new("email",[@core.Required,@core.Email]),
      ]
      let errs = @core.validate_map(vals,rules)
      if errs.has_errors() { ctx.json(422, errs.to_json()) } else { ctx.json(201, Json::object(@core.h([("msg",Json::string("OK"))]))) }
    }
    None => ctx.json(400, Json::object(@core.h([("error",Json::string("bad JSON"))])))
  }
}

///| ——— Rendering ———

async fn h_json_list(ctx : @core.Context) -> Unit {
  ctx.json(200, Json::object(@core.h([("items",Json::array([
    Json::object(@core.h([("id",Json::number(1.0)),("name",Json::string("A"))])),
    Json::object(@core.h([("id",Json::number(2.0)),("name",Json::string("B"))])),
  ]))])))
}
async fn h_xml(ctx : @core.Context) -> Unit { ctx.xml(200,Json::object(@core.h([("msg",Json::string("hi"))]))) }
async fn h_yaml(ctx : @core.Context) -> Unit { ctx.yaml(200,Json::object(@core.h([("msg",Json::string("hi"))]))) }
async fn h_html(ctx : @core.Context) -> Unit { ctx.html(200,"

mbit HTML

from MoonBit

") } async fn h_redirect(ctx : @core.Context) -> Unit { ctx.redirect(301,"/api/hello") } async fn h_no_content(ctx : @core.Context) -> Unit { ctx.no_content() } ///| ——— SSE & Stream ——— async fn h_sse(ctx : @core.Context) -> Unit { ctx.stream_start(200, "text/event-stream") for i=0;i<5;i=i+1 { ctx.sse("msg","#"+i.to_string()); ctx.sse_flush() } ctx.sse("done","complete") ctx.end_stream() } async fn h_stream(ctx : @core.Context) -> Unit { ctx.stream(200, "text/plain", async fn(w : async (String) -> Unit) -> Unit { w("chunk-1\n"); w("chunk-2\n"); w("chunk-3\n") }) } ///| ——— Negotiation ——— async fn h_negotiate(ctx : @core.Context) -> Unit { let data = @core.h([("msg",Json::string("hello"))]) match ctx.negotiate_format(["application/json","text/html","text/plain"]) { Some("application/json") => ctx.json(200,Json::object(data)) Some("text/html") => ctx.html(200,"

hello

") Some("text/plain") => ctx.string(200,"hello") _ => ctx.json(406,Json::object(@core.h([("error",Json::string("not acceptable"))]))) } } ///| ——— Context Store ——— async fn h_store_set(ctx : @core.Context) -> Unit { ctx.set("shared",Json::string("stored-value")); ctx.next() } async fn h_store_get(ctx : @core.Context) -> Unit { ctx.json(200,Json::object(@core.h([("value",Json::string(ctx.get_string("shared").or("missing")))]))) } ///| ——— Files ——— async fn h_file(ctx : @core.Context) -> Unit { ctx.file_attachment("demo.txt") } async fn h_files_wc(ctx : @core.Context) -> Unit { ctx.json(200,Json::object(@core.h([("file",Json::string(ctx.param_default("filepath","")))]))) } ///| ——— Errors ——— async fn h_403(ctx : @core.Context) -> Unit { ctx.abort_with_status(403,"Access denied") } async fn h_404(ctx : @core.Context) -> Unit { ctx.json(404,Json::object(@core.h([("error",Json::string("Custom 404")),("path",Json::string(ctx.path()))]))) } async fn h_405(ctx : @core.Context) -> Unit { ctx.set_header("Allow","GET,POST"); ctx.json(405,Json::object(@core.h([("error",Json::string("Method Not Allowed"))]))) } async fn h_options_h(ctx : @core.Context) -> Unit { ctx.set_header("Allow","GET,POST,PUT,DELETE,PATCH,OPTIONS,HEAD"); ctx.no_content() } ///| ——— Main ——— async fn main { let app = @core.default() // Middleware app.use(@core.logger()) |> ignore app.use(@core.recovery()) |> ignore app.use(@middleware.cors(@middleware.CORSConfig::default())) |> ignore app.use(@middleware.request_id()) |> ignore app.use(@middleware.timing()) |> ignore // Error handlers app.no_route([h_404]) app.no_method([h_405]) // Routes app.get("/",[h_root]) |> ignore app.any("/api/health",[h_health]) |> ignore app.head("/api/health",[h_health]) |> ignore app.options("/api/health",[h_options_h]) |> ignore app.get("/api/hello",[h_hello]) |> ignore app.get("/api/hello/:name",[h_hello_name]) |> ignore app.get("/api/search",[h_search]) |> ignore app.get("/api/query-all",[h_query_all]) |> ignore app.post("/api/echo",[h_echo]) |> ignore app.post("/api/users",[h_create_user]) |> ignore app.get("/api/users/:id",[h_hello_name]) |> ignore app.put("/api/users/:id",[h_put_user]) |> ignore app.del("/api/users/:id",[h_delete_user]) |> ignore app.patch("/api/users/:id",[h_patch_user]) |> ignore app.post("/api/login",[h_login]) |> ignore app.post("/api/register",[h_register]) |> ignore app.get("/api/render/json",[h_json_list]) |> ignore app.get("/api/render/xml",[h_xml]) |> ignore app.get("/api/render/yaml",[h_yaml]) |> ignore app.get("/api/render/html",[h_html]) |> ignore app.get("/api/render/redirect",[h_redirect]) |> ignore app.get("/api/render/no-content",[h_no_content]) |> ignore app.get("/api/sse",[h_sse]) |> ignore app.get("/api/stream",[h_stream]) |> ignore app.get("/api/negotiate",[h_negotiate]) |> ignore app.get("/api/store",[h_store_set,h_store_get]) |> ignore app.get("/api/file",[h_file]) |> ignore app.get("/api/files/*filepath",[h_files_wc]) |> ignore app.get("/api/error/403",[h_403]) |> ignore @core.debug_print_listen("http://0.0.0.0:8080") app.run("0.0.0.0:8080") }