// oasviz.typ // // oasviz is a Typst library designed for visualizing OpenAPI Specification (OAS) documents. // It provides a suite of functions to render API endpoints, schemas, servers, and other // components in a structured and aesthetically pleasing format, ideal for technical // documentation and API reports. // #import "i18n.typ": translate #import "../util/tabler.typ" #import "../pallete.typ": palette // Helper to render a badge for HTTP methods #let method-badge(method) = { let lightness = "600" let color = if method == "get" { palette.blue.at(lightness) } else if method == "post" { palette.green.at(lightness) } else if method == "put" { palette.orange.at(lightness) } else if method == "delete" { palette.red.at(lightness) } else if method == "patch" { palette.purple.at(lightness) } else { palette.gray.at(lightness) } box( fill: color, stroke: none, radius: 2pt, inset: (x: 0.4em, y: 0.4em), text(weight: "bold", fill: white, size: 0.9em, upper(method)), ) } // Render Servers #let render-servers(servers) = { if servers != none and servers.len() > 0 [ = #translate("servers.title") #translate("servers.desc") #figure( table( columns: (auto, 1fr), stroke: none, align: left, translate("servers.table.addr"), translate("servers.table.desc"), table.hline(stroke: 1pt + palette.ui.border), ..( for server in servers { ( raw(server.url), if "description" in server { server.description } else { "-" }, ) } ), ), caption: translate("servers.caption"), ) ] } #let _get-type-display(val) = { if val == none { return translate("common.any") } if "$ref" in val { return raw(val.at("$ref").split("/").last()) } if "type" in val { if val.type == "array" and "items" in val { return [#translate("endpoints.array-of") #_get-type-display(val.items)] } return val.type } return translate("common.any") } #let _resolve-all-properties(schema, all-schemas, seen: ()) = { let props = (:) if "properties" in schema { props = props + schema.properties } if "allOf" in schema { for element in schema.allOf { if "$ref" in element { let ref-name = element.at("$ref").split("/").last() if ref-name not in seen { let ref-schema = all-schemas.at(ref-name, default: (:)) props = props + _resolve-all-properties(ref-schema, all-schemas, seen: seen + (ref-name,)) } } else if "properties" in element { props = props + element.properties } else if "allOf" in element { props = props + _resolve-all-properties(element, all-schemas, seen: seen) } } } return props } // Render a single Schema/Type #let render-schema(name, schema, all-schemas: (:)) = [ === #name #if "type" in schema { [ (#schema.type)] } #let props = _resolve-all-properties(schema, all-schemas) #if props.len() > 0 { figure( table( columns: (auto, auto, 1fr), align: (x, y) => left, table.header( [*#translate("schemas.property")*], [*#translate("schemas.type")*], [*#translate("schemas.desc-ex")*] ), ..props .pairs() .map(((key, val)) => { ( raw(key), _get-type-display(val), { if "description" in val { val.description + " " } if "example" in val { text(fill: palette.ui.text-light, [#translate("common.example"): #val.example]) } }, ) }) .flatten(), ), caption: [#translate("schemas.caption") #name], ) } else { sym.dash } ] // Render Components/Schemas #let render-components(components) = { if components != none and "schemas" in components { [ = #translate("schemas.title") #context if translate("schemas.desc") != none { par(translate("schemas.desc")) } ] let schemas = components.schemas for (name, schema) in schemas.pairs() { render-schema(name, schema, all-schemas: schemas) } } } #let _resolve-ref(val, pool) = { if type(val) == dictionary and "$ref" in val { let ref-name = val.at("$ref").split("/").last() return pool.at(ref-name, default: val) } return val } // Render a single Operation (Endpoint) #let render-operation(method, path, op, components: (:)) = [ #figure( tabler.sideways( columns: (auto, auto, auto, auto, 1fr), translate("endpoints.endpoint"), table.cell(colspan: 4, strong(path)), translate("endpoints.method"), table.cell(colspan: 4, method-badge(method)), ..if "summary" in op { (translate("endpoints.summary"), table.cell(colspan: 4, op.summary)) }, ..if "description" in op { (translate("endpoints.description"), table.cell(colspan: 4, op.description)) }, ..if "parameters" in op and op.parameters.len() > 0 { ( table.cell(rowspan: op.parameters.len(), translate("endpoints.parameters")), ..for p-raw in op.parameters { let p = _resolve-ref(p-raw, components.at("parameters", default: (:))) ( p.name, p.at("in", default: "query"), if "schema" in p { _get-type-display(p.schema) } else { "-" }, p.at("description", default: ""), ) }, ) }, ..if "requestBody" in op { let rb = _resolve-ref(op.requestBody, components.at("requestBodies", default: (:))) ( translate("endpoints.request-body"), ..for (media-type, details) in rb.at("content", default: (:)).pairs() { ( table.cell( colspan: 2, raw(media-type), ), table.cell(colspan: 2, if "schema" in details { [#translate("endpoints.type-prefix"): #_get-type-display(details.schema)] } else { "-" }), ) }, ) }, ..if "responses" in op { ( table.cell(rowspan: op.responses.len(), translate("endpoints.responses")), ..op .responses .pairs() .map(((code, resp-raw)) => { let resp = _resolve-ref(resp-raw, components.at("responses", default: (:))) let schema-display = [] if "content" in resp { let content-map = resp.content let media-type = if "application/json" in content-map { "application/json" } else { content-map.keys().at(0, default: none) } if media-type != none { let media-obj = content-map.at(media-type) if "schema" in media-obj { schema-display = [#translate("endpoints.type-prefix"): #_get-type-display(media-obj.schema)] } } } ( strong(code), table.cell(colspan: 3, [ #resp.at("description", default: "") #if schema-display != [] [ \ #text(fill: palette.ui.text-light, schema-display) ] ]), ) }) .flatten(), ) }, ), caption: [#translate("endpoints.caption") #upper(method) #path], ) ] // Main Render Function #let render(data) = { show figure.where(kind: table): set block(breakable: false) let components = data.at("components", default: (:)) // Servers if "servers" in data { render-servers(data.servers) } // Paths if "paths" in data { [ = #translate("endpoints.title") #context if translate("endpoints.desc") != none { par(translate("endpoints.desc")) } ] for (path, item) in data.paths.pairs() { let methods = ("get", "post", "put", "delete", "patch", "head", "options", "trace") for method in methods { if method in item { render-operation(method, path, item.at(method), components: components) } } } } // Components if "components" in data { render-components(data.components) } }