net/http/swagger
Writing OpenAPI documentation for an API and serving the documentation page.
import net.http.swagger
This package writes the documentation of an HTTP API as an OpenAPI 3.1 document and serves it with a viewer page where people can read each operation and try it. Use it with an API built on net/http and net/router: client generators, API gateways and your own front-end team read the same openapi.json. The viewer is built into the package, so it works offline and loads nothing from a CDN.
How it fits together:
- A
Docholds the whole document.Routeadds an operation (orAtpicks one that exists), and the calls after it, such asSummary,QueryandReturns, describe that operation. - Body and response schemas come from a sample value: pass an empty struct and its fields, under their
@jsonnames, become a schema undercomponents. Mountadds the viewer and the spec to your router;Problems,MissingandUndocumentedcheck the document in a test.
Things that catch people out:
Mountcopies the spec when you call it, and adds routes to the router. Describe every route first, thenMount, thenhttp.Use; a description added afterMountnever reaches the page.- After an
Atthat finds nothing, the following calls do nothing. Check whatAtreturns, or useRoute, which creates the operation. - The docs are public. Anyone who can reach the server can read
/docs, and the spec is served withAccess-Control-Allow-Origin: *. LeaveMountout of a production build if the API is private.
Document an API next to its routes
The simplest habit is to describe each route right after you register it, so the two never drift apart. Route starts an operation, and the calls after it fill it in. Returns takes a sample value: pass a struct, or an array of one struct for a list, and the schema appears under components. Mount then serves the spec at /docs/openapi.json and the viewer at /docs.
import net.http
import net.http.swagger
import net.router
import encoding.json
struct Task:
@json "id"
pub id: int = 0
@json "title"
pub title: str = ""
@json "done"
pub done: bool = false
end
func list(ctx: i64):
http.Json(ctx, `[{"id":1,"title":"write docs","done":false}]`)
end
func create(ctx: i64):
http.SetStatus(ctx, 201)
http.Json(ctx, http.Body(ctx))
end
func main:
mut r := router.NewRouter()
mut doc := swagger.New("tasks", "1.0.0")
router.Get(r, "/tasks", list)
_ := swagger.Route(doc, "GET", "/tasks")
swagger.Summary(doc, "all tasks")
swagger.Tag(doc, "tasks")
swagger.Returns(doc, 200, "the tasks", [Task {}])
router.Post(r, "/tasks", create)
_ := swagger.Route(doc, "POST", "/tasks")
swagger.Summary(doc, "add a task")
swagger.Tag(doc, "tasks")
swagger.Accepts(doc, "the new task", Task {})
swagger.Returns(doc, 201, "the task as stored", Task {})
println "missing:", swagger.Missing(doc, r)
swagger.Mount(doc, r, "/docs")
mut s := http.NewServer(19774)
http.Use(s, r)
_ := http.RunBackground(s)
spec := http.Body(http.Get("http://127.0.0.1:19774/docs/openapi.json"))
println json.Keys(json.Get(spec, "paths"))
println json.GetPath(spec, "paths./tasks.post.summary")
println json.GetPath(spec, "paths./tasks.get.responses.200.description")
println json.GetPath(spec, "paths./tasks.get.responses.200.content")
println json.Keys(json.GetPath(spec, "components.schemas.Task.properties"))
println http.Status(http.Get("http://127.0.0.1:19774/docs"))
swagger.Free(doc)
endMount takes a copy of the spec when you call it, so document every route first and mount last. Path patterns are written the router's way, /books/:id, and appear in OpenAPI's form, /books/{id}.
Security schemes and saving the spec
Declare each way to sign in once, with BearerAuth for tokens in the Authorization header or ApiKeyAuth for a key in a header, query value or cookie. Then mark each operation that needs it with Secure and the scheme's name. WriteSpec saves the spec to a file, for a client generator or for publishing with the site, and SpecFromFile reads one back.
import net.http.swagger
import encoding.json
struct Invoice:
@json "number"
pub number: str = ""
@json "total"
pub total: f64 = 0.0
end
func main:
mut doc := swagger.New("billing", "1.4.0")
swagger.Server(doc, "https://billing.example.com", "production")
swagger.BearerAuth(doc, "user", "JWT")
swagger.ApiKeyAuth(doc, "partner", "header", "X-Partner-Key")
_ := swagger.Route(doc, "GET", "/invoices/:number")
swagger.Summary(doc, "one invoice")
swagger.Secure(doc, "user")
swagger.Returns(doc, 200, "the invoice", Invoice {})
swagger.ReturnsNothing(doc, 401, "no valid token")
_ := swagger.Route(doc, "POST", "/partner/invoices")
swagger.Summary(doc, "a partner sends an invoice")
swagger.Secure(doc, "partner")
swagger.Accepts(doc, "the invoice", Invoice {})
swagger.ReturnsNothing(doc, 202, "accepted")
println "problems:", swagger.Problems(doc).len()
println swagger.WriteSpec(doc, "openapi.json")
spec := swagger.SpecFromFile("openapi.json")
println json.Minify(json.GetPath(spec, "components.securitySchemes.user"))
println json.Minify(json.GetPath(spec, "components.securitySchemes.partner"))
println json.Minify(json.GetPath(spec, "paths./invoices/{number}.get.security"))
println json.GetPath(spec, "components.schemas.Invoice.properties.total.type")
swagger.Free(doc)
endCheck the documentation in CI
Documentation goes stale quietly. Problems finds mistakes in the spec itself: a security scheme that was never declared, a path parameter that does not match the path, a status code outside 100 to 599, a body on a GET, or a repeated operation id. Missing compares the spec with the router and lists the routes that have no operation. Print both in a test and fail the build when either is not empty.
import net.http
import net.http.swagger
import net.router
func any(ctx: i64):
http.Text(ctx, "")
end
func main:
mut r := router.NewRouter()
router.Get(r, "/users/:id", any)
router.Delete(r, "/users/:id", any)
router.Get(r, "/reports", any)
mut doc := swagger.New("users", "2.0.0")
_ := swagger.Route(doc, "GET", "/users/:id")
swagger.Summary(doc, "one user")
swagger.Secure(doc, "token")
swagger.ReturnsText(doc, 200, "the user")
_ := swagger.Route(doc, "DELETE", "/users/:id")
swagger.PathParam(doc, "id", "the user id")
swagger.ReturnsNothing(doc, 2040, "deleted")
mut failed := 0
each p in swagger.Problems(doc):
println "problem:", p
failed += 1
end
each m in swagger.Missing(doc, r):
println "not documented:", m
failed += 1
end
println failed, "issue(s)", failed > 0 ? "so the build fails" : "so the build passes"
swagger.Free(doc)
endIn a real check, return 1 from main when the count is above zero, so the CI job fails. Undocumented is a softer check: it lists operations that exist but have neither a summary, a description nor a response.
Serve the docs with the API
For an existing router, Scan adds an empty operation for every route, so nothing is forgotten. At then picks one of them to describe. AcceptsRaw and ReturnsRaw take a content type and a JSON Schema as text, for bodies that are not a struct.
import net.http
import net.http.swagger
import net.router
func ping(ctx: i64):
http.Text(ctx, "pong")
end
func echo(ctx: i64):
http.Json(ctx, http.Body(ctx))
end
func main:
mut r := router.NewRouter()
router.Get(r, "/ping", ping)
router.Post(r, "/echo", echo)
mut doc := swagger.New("echo service", "0.1.0")
swagger.Describe(doc, "Answers pings and echoes JSON back.")
swagger.Server(doc, "http://localhost:8080", "this machine")
swagger.Scan(doc, r)
_ := swagger.At(doc, "GET", "/ping")
swagger.Summary(doc, "is the service up")
swagger.ReturnsText(doc, 200, "always pong")
_ := swagger.At(doc, "POST", "/echo")
swagger.Summary(doc, "send JSON, get the same JSON back")
swagger.AcceptsRaw(doc, "any JSON value", swagger.JSON_TYPE, "{}", true)
swagger.ReturnsRaw(doc, 200, "the same value", swagger.JSON_TYPE, "{}")
swagger.Mount(doc, r, "/docs")
mut s := http.NewServer(8080)
http.Use(s, r)
println "docs at http://localhost:8080/docs"
http.Run(s)
endStart it with salam run and open http://localhost:8080/docs in a browser: the page lists both operations, and the Try it box under each one sends real requests to the running server. If you do not have a router at all, Serve runs a small server with only the docs, and ServeSpec does the same for a spec written by hand.
Types
Param
struct Param| Name | Type | Description |
|---|---|---|
name | str = "" | The name. |
loc | str = "query" | Where it sits: query, header, cookie or path. |
required | bool = false | It is required. |
description | str = "" | The description. |
schema | str = "{"type":"string"}" | The schema. |
import net.http.swagger
import encoding.json
struct Book:
@json "id"
pub id: int = 0
@json "title"
pub title: str = ""
end
func main:
mut doc := swagger.New("library", "1.2.0")
swagger.Describe(doc, "a sample API for books")
swagger.Server(doc, "https://api.example.com", "the main one")
swagger.Contact(doc, "the Salam team", "https://salamlang.ir", "team@salamlang.ir")
swagger.License(doc, "MIT", "https://opensource.org/licenses/MIT")
swagger.BearerAuth(doc, "token", "JWT")
swagger.ApiKeyAuth(doc, "key", "header", "X-API-Key")
_ := swagger.Route(doc, "GET", "/books/:id")
swagger.Summary(doc, "one book")
swagger.Details(doc, "Returns the book with this id.")
swagger.Tag(doc, "books")
swagger.OperationId(doc, "getBook")
swagger.PathParam(doc, "id", "the id of the book")
swagger.Query(doc, "fields", "the fields asked for", false)
swagger.QueryTyped(doc, "limit", "the greatest number", false, 10)
mut orders := Vector {} as Vector<str>
orders.push("title")
orders.push("id")
swagger.QueryEnum(doc, "sort", "the order", false, orders)
swagger.Header(doc, "X-Trace", "the trace id", false)
swagger.Cookie(doc, "sid", "the session", false)
swagger.Secure(doc, "token")
swagger.Returns(doc, 200, "the book", Book {})
swagger.ReturnsHeader(doc, 200, "ETag", "the version")
swagger.ReturnsNothing(doc, 404, "not found")
_ := swagger.Route(doc, "POST", "/books")
swagger.Accepts(doc, "the new book", Book {})
swagger.Returns(doc, 201, "created", Book {})
_ := swagger.Route(doc, "PUT", "/books/:id/cover")
swagger.AcceptsRaw(doc, "the cover image", "image/png", "{\"type\":\"string\",\"format\":\"binary\"}", true)
swagger.ReturnsText(doc, 200, "done")
_ := swagger.Route(doc, "POST", "/books/:id/notes")
swagger.AcceptsOptional(doc, "the note", Book {})
swagger.AcceptsForm(doc, "the form", Book {})
swagger.ReturnsRaw(doc, 200, "the export", "text/csv", "{\"type\":\"string\"}")
swagger.ReturnsHtml(doc, 200, "the page")
swagger.ReturnsFile(doc, 200, "the file", "application/pdf")
swagger.Deprecated(doc)
println swagger.At(doc, "get", "/books/:id"), swagger.At(doc, "GET", "/nope")
println doc.ops.len(), doc.ops[0].params.len(), doc.ops[0].replies.len(), doc.ops[3].is_deprecated, doc.schemes.len()
text := swagger.Spec(doc)
println json.Valid(text), json.Get(text, "openapi")
println swagger.SpecIndent(doc).len() > text.len()
println swagger.Problems(doc).len(), swagger.Undocumented(doc)
println swagger.WriteSpec(doc, "openapi.json"), swagger.SpecFromFile("openapi.json").len() > 0, swagger.SpecFromFile("missing.json")
println swagger.TEXT_SCHEMA, swagger.JSON_TYPE
swagger.Free(doc)
endReply
struct Reply| Name | Type | Description |
|---|---|---|
code | int = 200 | The status code. |
description | str = "" | The description. |
schema | str = "" | The schema of the body. |
content_type | str = "application/json" | The content type. |
header_names | Vector | The names of the response headers. |
header_notes | Vector | The note on each of them. |
The example for this item is shown with the ones above: net-http-swagger-Build.salam
Scheme
struct Scheme| Name | Type | Description |
|---|---|---|
name | str = "" | The name. |
kind | str = "http" | The kind: http or apiKey. |
scheme | str = "bearer" | For http, such as bearer. |
format | str = "" | The token format, such as JWT. |
loc | str = "header" | Where the key sits. |
param | str = "" | The name of the key parameter. |
description | str = "" | The description. |
The example for this item is shown with the ones above: net-http-swagger-Build.salam
Op
struct Op| Name | Type | Description |
|---|---|---|
method | str = "" | The method. |
pattern | str = "" | The path pattern. |
op_id | str = "" | The operation id. |
summary | str = "" | The summary. |
description | str = "" | The description. |
is_deprecated | bool = false | It is deprecated. |
tags | Vector | The tags. |
params | Vector | The parameters. |
replies | Vector | The responses. |
security | Vector | The authentication schemes it requires. |
body_schema | str = "" | The schema of the body. |
body_description | str = "" | The description of the body. |
body_required | bool = true | The body is required. |
body_content_type | str = "application/json" | The content type of the body. |
The example for this item is shown with the ones above: net-http-swagger-Build.salam
Definition
struct Definition| Name | Type | Description |
|---|---|---|
name | str = "" | The name. |
body | str = "" | The JSON schema. |
The example for this item is shown with the ones above: net-http-swagger-Build.salam
Doc
struct Doc| Name | Type | Description |
|---|---|---|
title | str = "API" | The title of the API. |
version | str = "1.0.0" | The version. |
description | str = "" | The description. |
servers | Vector | The server addresses. |
server_notes | Vector | The note on each server. |
ops | Vector | The operations. |
defs | Vector | The type definitions. |
schemes | Vector | The authentication schemes. |
contact_name | str = "" | The contact name. |
contact_url | str = "" | The contact address. |
contact_email | str = "" | The contact email. |
license_name | str = "" | The licence name. |
license_url | str = "" | The licence address. |
cur | int = -1 | The index of the current operation; the describing functions act on that one. |
The example for this item is shown with the ones above: net-http-swagger-Build.salam
Constants and variables
JSON_TYPE
const JSON_TYPE: str = "application/json"The example for this item is shown with the ones above: net-http-swagger-Build.salam
TEXT_TYPE
const TEXT_TYPE: str = "text/plain; charset=utf-8"The example for this item is shown with the ones above: net-http-swagger-Build.salam
HTML_TYPE
const HTML_TYPE: str = "text/html; charset=utf-8"The example for this item is shown with the ones above: net-http-swagger-Build.salam
FORM_TYPE
const FORM_TYPE: str = "application/x-www-form-urlencoded"The example for this item is shown with the ones above: net-http-swagger-Build.salam
TEXT_SCHEMA
const TEXT_SCHEMA: str = "{"type":"string"}"The example for this item is shown with the ones above: net-http-swagger-Build.salam
BINARY_SCHEMA
const BINARY_SCHEMA: str = "{"type":"string","format":"binary"}"The example for this item is shown with the ones above: net-http-swagger-Build.salam
Functions
Scan
func Scan(d &: Doc, r: router.Router)import net.http
import net.http.swagger
import net.router
import str
func list(ctx: i64):
http.Json(ctx, "[]")
end
func main:
mut r := router.NewRouter()
router.Get(r, "/books", list)
router.Post(r, "/books", list)
router.Get(r, "/admin/stats", list)
mut doc := swagger.New("library", "1.0.0")
println swagger.Missing(doc, r)
swagger.ScanPrefix(doc, r, "/books")
println doc.ops.len(), swagger.Missing(doc, r)
swagger.Scan(doc, r)
println doc.ops.len(), swagger.Missing(doc, r).len()
swagger.Mount(doc, r, "/docs")
mut server := http.NewServer(19627)
http.Use(server, r)
_ := http.RunBackground(server)
spec := http.Get("http://127.0.0.1:19627/docs/openapi.json")
println http.Status(spec), str.Contains(http.Body(spec), "/admin/stats")
page := http.Get("http://127.0.0.1:19627/docs")
println str.Contains(http.Body(page), "<html")
mut r2 := router.NewRouter()
swagger.MountSpec("{\"openapi\":\"3.1.0\"}", "ready made", r2, "/spec")
println r2.routes.len()
println swagger.Serve(doc, 19628), http.Status(http.Get("http://127.0.0.1:19628/openapi.json"))
println swagger.ServeSpec("{\"openapi\":\"3.1.0\"}", "another", 19629)
println str.Contains(swagger.Page("API", "/openapi.json"), "/openapi.json")
endScanPrefix
func ScanPrefix(d &: Doc, r: router.Router, prefix: str)The example for this item is shown with the ones above: net-http-swagger-Serve.salam
Page
func Page(title: str, spec_url: str): strThe example for this item is shown with the ones above: net-http-swagger-Serve.salam
Spec
func Spec(d: Doc): strThe example for this item is shown with the ones above: net-http-swagger-Build.salam
SpecIndent
func SpecIndent(d: Doc): strThe example for this item is shown with the ones above: net-http-swagger-Build.salam
Mount
func Mount(d: Doc, r &: router.Router, prefix: str)openapi.json to the router under the given prefix, with the spec as it is at this moment. Call it before http.Use.The example for this item is shown with the ones above: net-http-swagger-Serve.salam
MountSpec
func MountSpec(spec: str, title: str, r &: router.Router, prefix: str)The example for this item is shown with the ones above: net-http-swagger-Serve.salam
Serve
func Serve(d: Doc, port: int): boolThe example for this item is shown with the ones above: net-http-swagger-Serve.salam
ServeSpec
func ServeSpec(spec: str, title: str, port: int): boolThe example for this item is shown with the ones above: net-http-swagger-Serve.salam
SpecFromFile
func SpecFromFile(path: str): strThe example for this item is shown with the ones above: net-http-swagger-Build.salam
WriteSpec
func WriteSpec(d: Doc, path: str): boolThe example for this item is shown with the ones above: net-http-swagger-Build.salam
Problems
func Problems(d: Doc): Vector<str>The example for this item is shown with the ones above: net-http-swagger-Build.salam
Undocumented
func Undocumented(d: Doc): Vector<str>The example for this item is shown with the ones above: net-http-swagger-Build.salam
Missing
func Missing(d: Doc, r: router.Router): Vector<str>The example for this item is shown with the ones above: net-http-swagger-Serve.salam
Free
func Free(d &: Doc)The example for this item is shown with the ones above: net-http-swagger-Build.salam
New
func New(title: str, version: str): DocThe example for this item is shown with the ones above: net-http-swagger-Build.salam
Describe
func Describe(d &: Doc, text: str)The example for this item is shown with the ones above: net-http-swagger-Build.salam
Server
func Server(d &: Doc, url: str, note: str)The example for this item is shown with the ones above: net-http-swagger-Build.salam
Contact
func Contact(d &: Doc, name: str, url: str, email: str)The example for this item is shown with the ones above: net-http-swagger-Build.salam
License
func License(d &: Doc, name: str, url: str)The example for this item is shown with the ones above: net-http-swagger-Build.salam
BearerAuth
func BearerAuth(d &: Doc, name: str, format: str)The example for this item is shown with the ones above: net-http-swagger-Build.salam
ApiKeyAuth
func ApiKeyAuth(d &: Doc, name: str, loc: str, param: str)The example for this item is shown with the ones above: net-http-swagger-Build.salam
Route
func Route(d &: Doc, method: str, pattern: str): int:name parameters of the path are registered by themselves.The example for this item is shown with the ones above: net-http-swagger-Build.salam
At
func At(d &: Doc, method: str, pattern: str): boolThe example for this item is shown with the ones above: net-http-swagger-Build.salam
Summary
func Summary(d &: Doc, text: str)The example for this item is shown with the ones above: net-http-swagger-Build.salam
Details
func Details(d &: Doc, text: str)The example for this item is shown with the ones above: net-http-swagger-Build.salam
Tag
func Tag(d &: Doc, name: str)The example for this item is shown with the ones above: net-http-swagger-Build.salam
OperationId
func OperationId(d &: Doc, id: str)The example for this item is shown with the ones above: net-http-swagger-Build.salam
Deprecated
func Deprecated(d &: Doc)The example for this item is shown with the ones above: net-http-swagger-Build.salam
Secure
func Secure(d &: Doc, scheme: str)The example for this item is shown with the ones above: net-http-swagger-Build.salam
Query
func Query(d &: Doc, name: str, description: str, required: bool)The example for this item is shown with the ones above: net-http-swagger-Build.salam
QueryTyped
func QueryTyped<T>(d &: Doc, name: str, description: str, required: bool, sample: T)The example for this item is shown with the ones above: net-http-swagger-Build.salam
QueryEnum
func QueryEnum(d &: Doc, name: str, description: str, required: bool, values: Vector<str>)The example for this item is shown with the ones above: net-http-swagger-Build.salam
Header
func Header(d &: Doc, name: str, description: str, required: bool)The example for this item is shown with the ones above: net-http-swagger-Build.salam
Cookie
func Cookie(d &: Doc, name: str, description: str, required: bool)The example for this item is shown with the ones above: net-http-swagger-Build.salam
PathParam
func PathParam(d &: Doc, name: str, description: str)The example for this item is shown with the ones above: net-http-swagger-Build.salam
Accepts
func Accepts<T>(d &: Doc, description: str, sample: T)The example for this item is shown with the ones above: net-http-swagger-Build.salam
AcceptsOptional
func AcceptsOptional<T>(d &: Doc, description: str, sample: T)The example for this item is shown with the ones above: net-http-swagger-Build.salam
AcceptsRaw
func AcceptsRaw(d &: Doc, description: str, content_type: str, schema: str, required: bool)The example for this item is shown with the ones above: net-http-swagger-Build.salam
AcceptsForm
func AcceptsForm<T>(d &: Doc, description: str, sample: T)The example for this item is shown with the ones above: net-http-swagger-Build.salam
Returns
func Returns<T>(d &: Doc, code: int, description: str, sample: T)The example for this item is shown with the ones above: net-http-swagger-Build.salam
ReturnsRaw
func ReturnsRaw(d &: Doc, code: int, description: str, content_type: str, schema: str)The example for this item is shown with the ones above: net-http-swagger-Build.salam
ReturnsText
func ReturnsText(d &: Doc, code: int, description: str)The example for this item is shown with the ones above: net-http-swagger-Build.salam
ReturnsHtml
func ReturnsHtml(d &: Doc, code: int, description: str)The example for this item is shown with the ones above: net-http-swagger-Build.salam
ReturnsFile
func ReturnsFile(d &: Doc, code: int, description: str, content_type: str)The example for this item is shown with the ones above: net-http-swagger-Build.salam
ReturnsHeader
func ReturnsHeader(d &: Doc, code: int, name: str, description: str)The example for this item is shown with the ones above: net-http-swagger-Build.salam
ReturnsNothing
func ReturnsNothing(d &: Doc, code: int, description: str)The example for this item is shown with the ones above: net-http-swagger-Build.salam