
OpenApi.pudu
Pudu245 lines10.4 KB
1/** @Docgen.Rest.OpenApi — HTTP interface descriptions read into operations and schemas */2module PuduLangDocgen.Rest.OpenApi34import Std.List as List5import Std.Option as Option6import PuduLangDocgen as Docgen7import PuduLangDocgen.Meta as Meta89/** @Docgen.Rest.Parameter — one request parameter and where it is sent */10export type Parameter = { name: Str, location: Str, required: Bool, kind: Str, description: Str }1112/** @Docgen.Rest.Payload — a body for one media type */13export type Payload = { media: Str, kind: Str }1415/** @Docgen.Rest.Response — one documented response */16export type Response = { status: Str, description: Str, payloads: Array[Payload] }1718/** @Docgen.Rest.Operation — one method on one path */19export type Operation = {20 id: Str,21 method: Str,22 path: Str,23 summary: Str,24 description: Str,25 tags: Array[Str],26 parameters: Array[Parameter],27 body: Array[Payload],28 bodyRequired: Bool,29 responses: Array[Response],30 deprecated: Bool31}3233/** @Docgen.Rest.Property — one property of an object schema */34export type Property = { name: Str, kind: Str, required: Bool, description: Str }3536/** @Docgen.Rest.Schema — a named schema with its properties or shape */37export type Schema = { name: Str, kind: Str, description: Str, properties: Array[Property], values: Array[Str] }3839/** @Docgen.Rest.Service — a described HTTP interface */40export type Service = { title: Str, version: Str, description: Str, servers: Array[Str], operations: Array[Operation], schemas: Array[Schema] }414243const METHODS: Array[Str] = ["get", "put", "post", "delete", "options", "head", "patch", "trace"]444546export fn recognizes(value: &Docgen.Meta) -> Bool {47 match value {48 case Docgen.Fields(fields) => Option.isSome(&Meta.get(&fields, "openapi")) || Option.isSome(&Meta.get(&fields, "swagger"))49 case _ => false50 }51}525354export fn read(value: &Docgen.Meta) -> Result[Service, Str] {55 let root = match value {56 case Docgen.Fields(fields) => fields57 case _ => { return Err("interface description must be an object") }58 }59 let info = Meta.fieldsOf(&root, "info")60 let title = Meta.textOr(&info, "title", "")61 if title.trim().isEmpty() { return Err("interface description needs info.title") }62 var servers: Array[Str] = []63 match Meta.get(&root, "servers") {64 case Some(Docgen.Items(listed)) => {65 for server in listed {66 match server {67 case Docgen.Fields(fields) => { servers = servers.push(Meta.textOr(&fields, "url", "")) }68 case _ => {}69 }70 }71 }72 case _ => {73 let host = Meta.textOr(&root, "host", "")74 if !host.isEmpty() { servers = servers.push("https://" + host + Meta.textOr(&root, "basePath", "")) }75 }76 }77 var operations: Array[Operation] = []78 for (path, item) in Meta.fieldsOf(&root, "paths") {79 let entries = match item {80 case Docgen.Fields(found) => found81 case _ => { return Err("path " + path + " must be an object") }82 }83 let shared = parameters(&root, &entries)84 for method in METHODS {85 let found = Meta.fieldsOf(&entries, method)86 if !found.isEmpty() { operations = operations.push(operation(&root, path, method, &found, &shared)) }87 }88 }89 let definitions = if Meta.get(&root, "swagger") != None { Meta.fieldsOf(&root, "definitions") } else { Meta.fieldsOf(&Meta.fieldsOf(&root, "components"), "schemas") }90 var schemas: Array[Schema] = []91 for (name, held) in definitions { schemas = schemas.push(schema(name, &held)) }92 let sorted = List.sortOn(&schemas, |held: Schema| held.name)93 Ok(Service{title: title, version: Meta.textOr(&info, "version", ""), description: Meta.textOr(&info, "description", ""), servers: servers, operations: operations, schemas: sorted})94}959697export fn describe(value: &Docgen.Meta) -> Str {98 let fields = match value {99 case Docgen.Fields(found) => found100 case _ => { return "any" }101 }102 let reference = Meta.textOr(&fields, "$ref", "")103 if !reference.isEmpty() { return refName(reference) }104 let kind = Meta.textOr(&fields, "type", "")105 if kind == "array" { return "array of " + describe(&Option.unwrapOr(Meta.get(&fields, "items"), Docgen.Nothing)) }106 for combiner in ["oneOf", "anyOf", "allOf"] {107 match Meta.get(&fields, combiner) {108 case Some(Docgen.Items(options)) => { return options.map(|option: Docgen.Meta| describe(&option)).join(if combiner == "allOf" { " and " } else { " or " }) }109 case _ => {}110 }111 }112 let format = Meta.textOr(&fields, "format", "")113 let base = if kind.isEmpty() { if Meta.get(&fields, "properties") != None { "object" } else { "any" } } else { kind }114 if format.isEmpty() { base } else { base + " (" + format + ")" }115}116117118export fn refName(reference: Str) -> Str {119 let parts = reference.split("/")120 parts[parts.length() - 1]121}122123124fn dereferenced(root: &Array[(Str, Docgen.Meta)], value: &Docgen.Meta) -> Docgen.Meta {125 let fields = match value {126 case Docgen.Fields(found) => found127 case _ => { return *value }128 }129 let reference = Meta.textOr(&fields, "$ref", "")130 if !reference.startsWith("#/") { return *value }131 var current = Docgen.Fields(*root)132 for part in reference.drop(2).split("/") {133 current = match current {134 case Docgen.Fields(held) => Option.unwrapOr(Meta.get(&held, part.replace("~1", "/").replace("~0", "~")), Docgen.Nothing)135 case _ => Docgen.Nothing136 }137 }138 current139}140141142fn parameters(root: &Array[(Str, Docgen.Meta)], owner: &Array[(Str, Docgen.Meta)]) -> Array[Parameter] {143 var result: Array[Parameter] = []144 match Meta.get(owner, "parameters") {145 case Some(Docgen.Items(listed)) => {146 for held in listed {147 match dereferenced(root, &held) {148 case Docgen.Fields(fields) => {149 let schemaValue = Option.unwrapOr(Meta.get(&fields, "schema"), Docgen.Fields(fields))150 let location = Meta.textOr(&fields, "in", "")151 result = result.push(Parameter{name: Meta.textOr(&fields, "name", ""), location: location, required: Meta.flag(&fields, "required") || location == "path", kind: describe(&schemaValue), description: Meta.textOr(&fields, "description", "")})152 }153 case _ => {}154 }155 }156 }157 case _ => {}158 }159 result160}161162163fn payloads(fields: &Array[(Str, Docgen.Meta)]) -> Array[Payload] {164 var result: Array[Payload] = []165 for (media, held) in Meta.fieldsOf(fields, "content") {166 let kind = match held {167 case Docgen.Fields(inner) => describe(&Option.unwrapOr(Meta.get(&inner, "schema"), Docgen.Nothing))168 case _ => "any"169 }170 result = result.push(Payload{media: media, kind: kind})171 }172 match Meta.get(fields, "schema") {173 case Some(held) => { result = result.push(Payload{media: "application/json", kind: describe(&held)}) }174 case None => {}175 }176 result177}178179180fn operation(root: &Array[(Str, Docgen.Meta)], path: Str, method: Str, fields: &Array[(Str, Docgen.Meta)], shared: &Array[Parameter]) -> Operation {181 let own = parameters(root, fields)182 let inherited = shared.filter(|held: Parameter| own.filter(|mine: Parameter| mine.name == held.name && mine.location == held.location).isEmpty())183 let all = inherited.concat(own)184 let bodyParameters = all.filter(|held: Parameter| held.location == "body")185 var body: Array[Payload] = bodyParameters.map(|held: Parameter| Payload{media: "application/json", kind: held.kind})186 var bodyRequired = bodyParameters.filter(|held: Parameter| held.required).length() > 0187 match Meta.get(fields, "requestBody") {188 case Some(held) => {189 match dereferenced(root, &held) {190 case Docgen.Fields(inner) => {191 body = body.concat(payloads(&inner))192 bodyRequired = bodyRequired || Meta.flag(&inner, "required")193 }194 case _ => {}195 }196 }197 case None => {}198 }199 var responses: Array[Response] = []200 for (status, held) in Meta.fieldsOf(fields, "responses") {201 match dereferenced(root, &held) {202 case Docgen.Fields(inner) => { responses = responses.push(Response{status: status, description: Meta.textOr(&inner, "description", ""), payloads: payloads(&inner)}) }203 case _ => {}204 }205 }206 let tags = match Meta.get(fields, "tags") {207 case Some(Docgen.Items(listed)) => listed.map(|tag: Docgen.Meta| match tag { case Docgen.Text(written) => written case _ => "" }).filter(|tag: Str| !tag.isEmpty())208 case _ => []209 }210 let id = Meta.textOr(fields, "operationId", method + " " + path)211 Operation{id: id, method: method.toUpper(), path: path, summary: Meta.textOr(fields, "summary", ""), description: Meta.textOr(fields, "description", ""), tags: tags, parameters: all.filter(|held: Parameter| held.location != "body"), body: body, bodyRequired: bodyRequired, responses: responses, deprecated: Meta.flag(fields, "deprecated")}212}213214215fn schema(name: Str, value: &Docgen.Meta) -> Schema {216 let fields = match value {217 case Docgen.Fields(found) => found218 case _ => []219 }220 let required = match Meta.get(&fields, "required") {221 case Some(Docgen.Items(listed)) => listed.map(|held: Docgen.Meta| match held { case Docgen.Text(written) => written case _ => "" })222 case _ => []223 }224 var properties: Array[Property] = []225 for (key, held) in Meta.fieldsOf(&fields, "properties") {226 let description = match held {227 case Docgen.Fields(inner) => Meta.textOr(&inner, "description", "")228 case _ => ""229 }230 properties = properties.push(Property{name: key, kind: describe(&held), required: required.contains(key), description: description})231 }232 let values = match Meta.get(&fields, "enum") {233 case Some(Docgen.Items(listed)) => listed.map(fn(held: Docgen.Meta) -> Str {234 match held {235 case Docgen.Text(written) => written236 case Docgen.Whole(count) => show(count)237 case Docgen.Flag(flag) => if flag { "true" } else { "false" }238 case _ => "null"239 }240 })241 case _ => []242 }243 Schema{name: name, kind: describe(value), description: Meta.textOr(&fields, "description", ""), properties: properties, values: values}244}245