Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-docgen

Documentation publishing for Pudu: articles, API references, navigation, search, and static websites

0.1.0Apache-2.01

InstallClose

OpenApi.pudu

Pudu245 lines10.4 KB

GitHub ↗
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] }4142/// Methods an operation may use, in the order a path lists them.43const METHODS: Array[Str] = ["get", "put", "post", "delete", "options", "head", "patch", "trace"]4445/// Whether metadata is an HTTP interface description.46export 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}5253/// The service a description declares.54export 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}9596/// A short description of a schema's type, naming referenced schemas by name.97export 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}116117/// The schema name at the end of a local reference such as `#/components/schemas/Pet`.118export fn refName(reference: Str) -> Str {119  let parts = reference.split("/")120  parts[parts.length() - 1]121}122123/// The value a local reference points to, or the value itself when it is not a reference.124fn 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}140141/// Parameters declared on an object, following local references.142fn 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}161162/// Payloads of a `content` map, or of a Swagger `schema`.163fn 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}178179/// One operation; path-level parameters apply unless the operation redeclares them.180fn 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}213214/// A named schema with its properties, required names, and enumerated values.215fn 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