Skip to content

Commit 8364050

Browse files
committed
docs(examples): per-feature example suite — routing, OpenAPI, DI, OAuth2/JWT, forms, SSE, WebSocket (20 new).
Signed-off-by: 林晨 (Leo Cheng) <chengkelfan@qq.com>
1 parent 0db56dc commit 8364050

41 files changed

Lines changed: 1619 additions & 2 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/02-routing/main.mbt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
///|
2+
/// Routing across every HTTP verb, `:param` path extraction, and the 404-vs-405
3+
/// distinction. `App::get/post/put/patch/delete` bind a method to a path;
4+
/// `App::route` binds an explicit `Method`; a `:name` segment lands in
5+
/// `Context::param`. A path that exists under another verb answers 405, an
6+
/// unknown path answers 404.
7+
///
8+
/// moon run examples/02-routing
9+
#coverage.skip
10+
fn main {
11+
let app = @moonapi.App::new()
12+
app.get("/items/:id", ctx => {
13+
@moonapi.text(200, "item " + ctx.param("id").unwrap())
14+
})
15+
app.post("/items", _ctx => @moonapi.text(201, "created"))
16+
app.put("/items/:id", _ctx => @moonapi.text(200, "replaced"))
17+
app.patch("/items/:id", _ctx => @moonapi.text(200, "patched"))
18+
app.delete("/items/:id", _ctx => @moonapi.text(200, "deleted"))
19+
app.route(@moonapi.Head, "/items", _ctx => @moonapi.text(200, ""))
20+
app.route(@moonapi.Options, "/items", _ctx => @moonapi.text(204, ""))
21+
let hit = (verb : String, path : String) => {
22+
let req : @moonasgi.Request = {
23+
http_method: verb,
24+
path,
25+
query_string: b"",
26+
headers: [],
27+
body: b"",
28+
}
29+
let r = app.handle(req)
30+
println("\{verb} \{path} -> \{r.status} \{@utf8.decode_lossy(r.body[:])}")
31+
}
32+
hit("GET", "/items/233")
33+
hit("POST", "/items")
34+
hit("PUT", "/items/233")
35+
hit("PATCH", "/items/233")
36+
hit("DELETE", "/items/233")
37+
hit("HEAD", "/items")
38+
hit("OPTIONS", "/items")
39+
hit("GET", "/items") // path exists (POST/HEAD/OPTIONS) but not for GET -> 405
40+
hit("GET", "/missing") // no such path -> 404
41+
}

examples/02-routing/moon.pkg

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {
2+
"Lfan-ke/moonapi",
3+
"Lfan-ke/moonasgi",
4+
"moonbitlang/core/encoding/utf8",
5+
}
6+
7+
pkgtype(kind: "executable")
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
///|
2+
/// One set of typed routes, every mainstream spec version. `App::openapi_json`
3+
/// emits Swagger 2.0, OpenAPI 3.0.3, and OpenAPI 3.1.0 off the same descriptors;
4+
/// `App::openapi` additionally takes the document metadata (title, description,
5+
/// contact, license, servers) and `swagger_ui` returns a ready docs page. The
6+
/// `demo_app` carries typed bodies, so the emitted specs show `components`
7+
/// (`definitions` in 2.0) with `$ref`s.
8+
///
9+
/// moon run examples/03-openapi-versions
10+
#coverage.skip
11+
fn main {
12+
let app = @moonapi.demo_app()
13+
app.get(
14+
"/legacy",
15+
_ctx => @moonapi.text(200, "old"),
16+
summary="the legacy list",
17+
tags=["misc"],
18+
deprecated=true,
19+
)
20+
println("--- Swagger 2.0 (definitions) ---")
21+
println(app.openapi_json(version=@moonapi.Swagger20))
22+
println("--- OpenAPI 3.0.3 (components/schemas) ---")
23+
println(app.openapi_json(version=@moonapi.OpenApi30))
24+
println("--- OpenAPI 3.1.0 with full info metadata ---")
25+
let doc = app.openapi(
26+
version=@moonapi.OpenApi31,
27+
title="Widget API",
28+
api_version="2.3.0",
29+
description="a comprehensive moonapi example",
30+
contact=Some({ name: "lfanke", url: "", email: "heke1228@example.com" }),
31+
license=Some({
32+
name: "Apache-2.0",
33+
url: "https://www.apache.org/licenses/LICENSE-2.0",
34+
}),
35+
servers=[{ url: "https://api.example.com", description: "production" }],
36+
)
37+
println(doc.stringify(indent=2))
38+
println("swagger_ui page: \{@moonapi.swagger_ui().length()} bytes")
39+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {
2+
"Lfan-ke/moonapi",
3+
}
4+
5+
pkgtype(kind: "executable")

examples/04-descriptors/main.mbt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
///|
2+
/// The descriptor tree drives validation and OpenAPI from one source of truth.
3+
/// The `demo_app`'s `Endpoint` / `Param` / `Schema` / `ResponseSpec` descriptors
4+
/// validate an inbound `POST /users` body and a typed `:id` path param, and the
5+
/// same tree emits the request/response body schemas — objects, arrays, and a
6+
/// nested `Address` hoisted into `components/schemas` behind a `$ref`. A body
7+
/// that violates the descriptor comes back as a FastAPI-shaped `422`.
8+
///
9+
/// moon run examples/04-descriptors
10+
#coverage.skip
11+
fn main {
12+
let app = @moonapi.demo_app()
13+
let json_headers = [("content-type", "application/json")]
14+
let hit = (verb : String, path : String, body : String) => {
15+
let req : @moonasgi.Request = {
16+
http_method: verb,
17+
path,
18+
query_string: b"",
19+
headers: json_headers,
20+
body: @utf8.encode(body),
21+
}
22+
let r = app.handle(req)
23+
println("\{verb} \{path} -> \{r.status} \{@utf8.decode_lossy(r.body[:])}")
24+
}
25+
// A conforming body validates and deserialises.
26+
hit(
27+
"POST", "/users", "{\"name\":\"qwq\",\"email\":\"qwq@example.com\",\"age\":23}",
28+
)
29+
// Missing the required `name` -> 422 with a located `missing` error.
30+
hit("POST", "/users", "{\"email\":\"qwq@example.com\",\"age\":23}")
31+
// A typed integer path param returns the nested `User` (address + tags).
32+
hit("GET", "/users/233", "")
33+
println("--- OpenAPI 3.1.0 (note components/schemas + $ref) ---")
34+
println(app.openapi_json(version=@moonapi.OpenApi31))
35+
}

examples/04-descriptors/moon.pkg

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {
2+
"Lfan-ke/moonapi",
3+
"Lfan-ke/moonasgi",
4+
"moonbitlang/core/encoding/utf8",
5+
}
6+
7+
pkgtype(kind: "executable")

examples/05-constraints/main.mbt

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
///|
2+
/// Pydantic-style field constraints and the scalar schema types, emitted into
3+
/// the OpenAPI schema and enforced off one descriptor. Every `Constraint` is
4+
/// exercised — `Minimum` / `Maximum`, `ExclusiveMinimum` / `ExclusiveMaximum`,
5+
/// `MultipleOf`, `MinLength` / `MaxLength`, `Pattern`, `MinItems` / `MaxItems` —
6+
/// alongside the scalar types `SInt` / `SFloat` / `SStr` / `SBool` / `SNull` /
7+
/// `SArray` and a closed `SEnum`. A conforming body validates; a low/short body
8+
/// and a high/long body each return a `422` naming every violation by its
9+
/// pydantic error type.
10+
///
11+
/// moon run examples/05-constraints
12+
#coverage.skip
13+
fn main {
14+
let color = @moonapi.SEnum(@moonapi.SStr, [
15+
"red".to_json(),
16+
"green".to_json(),
17+
"blue".to_json(),
18+
])
19+
let schema = @moonapi.Schema::object("Order", [
20+
@moonapi.Field::new("qty", @moonapi.SInt, constraints=[
21+
@moonapi.Minimum(1.0),
22+
@moonapi.Maximum(99.0),
23+
]),
24+
@moonapi.Field::new("step", @moonapi.SInt, constraints=[
25+
@moonapi.MultipleOf(5.0),
26+
]),
27+
@moonapi.Field::new("ratio", @moonapi.SFloat, constraints=[
28+
@moonapi.ExclusiveMinimum(0.0),
29+
@moonapi.ExclusiveMaximum(1.0),
30+
]),
31+
@moonapi.Field::new("code", @moonapi.SStr, constraints=[
32+
@moonapi.MinLength(3),
33+
@moonapi.MaxLength(8),
34+
@moonapi.Pattern("^[a-z]+$"),
35+
]),
36+
@moonapi.Field::new("tags", @moonapi.Schema::array(@moonapi.SStr), constraints=[
37+
@moonapi.MinItems(1),
38+
@moonapi.MaxItems(3),
39+
]),
40+
@moonapi.Field::new("active", @moonapi.SBool),
41+
@moonapi.Field::new("color", color),
42+
// A field whose value must be JSON null.
43+
@moonapi.Field::new("note", @moonapi.SNull, required=false),
44+
])
45+
let ep = @moonapi.Endpoint::new(request_body=Some(schema))
46+
let app = @moonapi.App::new()
47+
app.post(
48+
"/orders",
49+
ctx => {
50+
let errs = ep.validate(ctx)
51+
if errs.length() > 0 {
52+
@moonapi.unprocessable(errs)
53+
} else {
54+
@moonapi.text(201, "order accepted")
55+
}
56+
},
57+
endpoint=Some(ep),
58+
)
59+
let hit = (label : String, body : String) => {
60+
let req : @moonasgi.Request = {
61+
http_method: "POST",
62+
path: "/orders",
63+
query_string: b"",
64+
headers: [("content-type", "application/json")],
65+
body: @utf8.encode(body),
66+
}
67+
let r = app.handle(req)
68+
println("\{label} -> \{r.status} \{@utf8.decode_lossy(r.body[:])}")
69+
}
70+
// Within every bound.
71+
hit(
72+
"ok ", "{\"qty\":5,\"step\":10,\"ratio\":0.5,\"code\":\"abc\",\"tags\":[\"a\",\"b\"],\"active\":true,\"color\":\"red\"}",
73+
)
74+
// Below the lower bounds, too short, wrong scalar types, off the enum.
75+
hit(
76+
"low ", "{\"qty\":0,\"step\":7,\"ratio\":1.5,\"code\":\"AB\",\"tags\":[],\"active\":\"yes\",\"color\":\"pink\",\"note\":5}",
77+
)
78+
// Above the upper bounds, too long, too many items.
79+
hit(
80+
"high", "{\"qty\":100,\"step\":5,\"ratio\":0.0,\"code\":\"toolongcode\",\"tags\":[\"a\",\"b\",\"c\",\"d\"],\"active\":false,\"color\":\"green\"}",
81+
)
82+
println("--- OpenAPI 3.1.0 (constraints + enum in the schema) ---")
83+
println(app.openapi_json(version=@moonapi.OpenApi31))
84+
}

examples/05-constraints/moon.pkg

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {
2+
"Lfan-ke/moonapi",
3+
"Lfan-ke/moonasgi",
4+
"moonbitlang/core/encoding/utf8",
5+
}
6+
7+
pkgtype(kind: "executable")

examples/06-typed-body/main.mbt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
///|
2+
/// The typed body extractors. `Context::body_validated[T]` checks the JSON body
3+
/// against a descriptor and only then deserialises into a `derive(FromJson)`
4+
/// struct, returning either the built value or a FastAPI-shaped `422`;
5+
/// `Context::body[T]` is the unchecked, total counterpart that yields `None`
6+
/// rather than erroring. `GreetReq` is a library struct with `derive(FromJson)`
7+
/// and a `schema()`.
8+
///
9+
/// moon run examples/06-typed-body
10+
#coverage.skip
11+
fn main {
12+
let app = @moonapi.App::new()
13+
// Validated extraction: conform, or get located errors.
14+
app.post("/greet", ctx => {
15+
match
16+
(
17+
ctx.body_validated(@moonapi.GreetReq::schema()) :
18+
Result[@moonapi.GreetReq, _]) {
19+
Ok(g) => @moonapi.text(200, "Hello, " + g.name)
20+
Err(errs) => @moonapi.unprocessable(errs)
21+
}
22+
})
23+
// Unchecked extraction: a total `T?`, `None` when the body cannot shape-match.
24+
app.post("/echo", ctx => {
25+
let parsed : @moonapi.GreetReq? = ctx.body()
26+
match parsed {
27+
Some(g) => @moonapi.text(200, "parsed name=" + g.name)
28+
None => @moonapi.text(200, "unparseable")
29+
}
30+
})
31+
let hit = (path : String, body : String) => {
32+
let req : @moonasgi.Request = {
33+
http_method: "POST",
34+
path,
35+
query_string: b"",
36+
headers: [("content-type", "application/json")],
37+
body: @utf8.encode(body),
38+
}
39+
let r = app.handle(req)
40+
println(
41+
"POST \{path} \{body} -> \{r.status} \{@utf8.decode_lossy(r.body[:])}",
42+
)
43+
}
44+
hit("/greet", "{\"name\":\"lfanke\"}") // 200
45+
hit("/greet", "{\"name\":233}") // 422 string_type
46+
hit("/greet", "") // 422 missing body
47+
hit("/echo", "{\"name\":\"qwq\"}") // parsed
48+
hit("/echo", "not json at all") // unparseable
49+
}

examples/06-typed-body/moon.pkg

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {
2+
"Lfan-ke/moonapi",
3+
"Lfan-ke/moonasgi",
4+
"moonbitlang/core/encoding/utf8",
5+
}
6+
7+
pkgtype(kind: "executable")

0 commit comments

Comments
 (0)