Gitlab Community Edition Instance

Skip to content
Snippets Groups Projects
openapi.xqm 11.6 KiB
Newer Older
Mathias Goebel's avatar
Mathias Goebel committed
xquery version "3.1";
(:~
 : This library provides functions to prepare an OpenAPI description file for
 : documenting public REST-APIs made with RESTXQ. Its output is a JSON file that
 : can be used with swagger-ui.
 : @author Mathias Göbel, SUB Göttingen
 : @version 1.0
 : @see https://www.openapis.org/
 :)

module namespace openapi="https://lab.sub.uni-goettingen.de/restxqopenapi";

declare namespace rest="http://exquery.org/ns/restxq";
declare namespace pkg="http://expath.org/ns/pkg";
declare namespace repo="http://exist-db.org/xquery/repo";

declare %private variable $openapi:supported-methods := ("rest:GET", "rest:HEAD", "rest:POST", "rest:PUT", "rest:DELETE");
Mathias Goebel's avatar
Mathias Goebel committed

(:~
 : Prepares a JSON document usually to be stored as "openapi.json".
 : @param $target the collection to prepare the descriptor for, e.g. “/db/apps/application”
 :   :)
declare function openapi:json($target as xs:string)
as xs:string {
  openapi:main($target)
  => serialize(map{ "method": "json", "media-type": "application/json" })
};

(:~
 : Prepare OpenAPI descriptor for an installed package specified by its path in
 : the database.
 : @param $target the collection to prepare the descriptor for, e.g. “/db/apps/application”
 : @return complete OpenAPI description
 :)
declare function openapi:main($target as xs:string)
as map(*) {
  let $modules-uris := collection($target)[ends-with(base-uri(), ".xqm")]/base-uri()
  let $module :=
    for $module in $modules-uris
    let $inspect := inspect:inspect-module($module)
    where $inspect/function/annotation/string(@name) = $openapi:supported-methods
    return
        $inspect

  let $config := doc($target || "/openapi-config.xml")/*
  let $repo := doc($target || "/repo.xml")/*
  let $expath := doc($target || "/expath-pkg.xml")/*
  return
    map:merge((
    map{"openapi": "3.0.1"},
    openapi:paths-object($module),
    openapi:servers-object($config/openapi:servers),
    openapi:info-object($expath, $repo, $config/openapi:info),
    openapi:tags-object($module)
    ))
};

(:~
 : Prepare OAS3 Info Object
 : @see https://swagger.io/specification/#infoObject
 :)
declare %private function openapi:info-object($expath as element(pkg:package), $repo as element(repo:meta), $config as element(openapi:info))
as map(*) {
  map{ "info":
    map{
      "title": string($expath/pkg:title),
      "description": string($repo/repo:description),
      "termsOfService": string($config/openapi:termsOfService),
      "contact": openapi:contact-object($repo, $config/openapi:contact),
      "license": openapi:license-object($repo),
      "version": string($expath/@version)
    }
  }
};

(:~
 : Prepare a OAS Contact Object
 : @see https://swagger.io/specification/#contactObject
 :)
declare %private function openapi:contact-object($repo as element(), $config as element(openapi:contact))
as map(*) {
    map{
        "name": string($repo/repo:author[1]),
        "url": string($repo/repo:website[1]),
        "email": string($config/openapi:email)
        }
};

(:~
 : Prepare a OAS License Object
 : @see https://swagger.io/specification/#licenseObject
 :)
declare %private function openapi:license-object($repo as element(repo:meta))
Mathias Goebel's avatar
Mathias Goebel committed
as map(*) {
  let $licenseId := string($repo/repo:license)
  let $url := (map:get(openapi:spdx($licenseId), "seeAlso")?*)[1]
  return
    map{
      "name": $licenseId,
      "url": $url,
      "x-name-is-spdx": exists($url)
      }
};

(:~
 : Prepares a OAS3 Servers Object
 : @see https://swagger.io/specification/#serverObject
 :)
declare %private function openapi:servers-object($config as element(openapi:servers))
Mathias Goebel's avatar
Mathias Goebel committed
as map(*) {
  map{
      "servers":[
        for $server in $config/openapi:server
        return
          map{
            "url": string($server/@url),
            "description": string($server)
          }
        ]
  }
};

(:~
 : Prepare OAS3 Paths Object.
 : @see https://swagger.io/specification/#pathsObject
 :)
declare %private function openapi:paths-object($module as element(module)+)
as map(*) {
  map{
    "paths":
      map:merge((
        $module/function[annotation/@name = "rest:path"] ! openapi:operation-object(.)
      ))
  }
};

(:~
 : Prepare OAS3 Operation Object.
 : @see https://swagger.io/specification/#operationObject
 :)
declare %private function openapi:operation-object($function as element(function))
Mathias Goebel's avatar
Mathias Goebel committed
as map(*) {
  let $desc := normalize-space($function/description)
  let $see := normalize-space($function/see)
  let $deprecated := $function/deprecated
  let $tags := array { $function/@name => substring-before(":") }
  return
  map{
Mathias Goebel's avatar
Mathias Goebel committed
    $function/annotation[@name eq "rest:path"]/replace(value, "\{\$", "{") :
Mathias Goebel's avatar
Mathias Goebel committed
    for $method in $function/annotation[@name = $openapi:supported-methods]/substring-after(lower-case(@name), "rest:")
    return
    map{
      $method:
      map:merge((
        map{ "description": $desc},
        map{ "tags": $tags},
        $see[1] ! map{"externalDocs": $see ! map{
          "url": .,
          "description": "the official documentation by the maintainer or a thrid-party documentation"}},
        $deprecated ! map{"deprecated": true()},
        openapi:parameter-object($function),
Mathias Goebel's avatar
Mathias Goebel committed
        openapi:responses-object($function),
        openapi:requestBody-object($function)
Mathias Goebel's avatar
Mathias Goebel committed
declare %private function openapi:requestBody-object($function as element(function))
as map(*)? {
if(not(exists($function/annotation[@name = ("rest:POST", "rest:PUT")]/value))) then () else
    let $name := replace($function/annotation[@name = ("rest:POST", "rest:PUT")]/value, "\{|\}|\$", "")
    let $desc := string($function/argument[@var eq $name])
    let $example := string(($function/annotation[@name="test:arg"][value[1] eq $name])[1]/value[2])
    return
    map{
        "requestBody":  map{
            "description": "Value to process as variable: $" || $name,
            "content": map{
                "application/xml": map{
                    "examples": map{
                        $name: map{
                            "summary": $desc,
                            "value": serialize($example)
                        }
                    }
                }
            },
            "required": true()
        }
    }
};

Mathias Goebel's avatar
Mathias Goebel committed
(:~
 : Prepare OAS3 Response Object.
 : @see https://swagger.io/specification/#responsesObject
 :  :)
declare %private function openapi:responses-object($function as element(function))
Mathias Goebel's avatar
Mathias Goebel committed
as map(*){
  map{
    "responses":
    map{
      "200": map{
        "description": string($function/returns),
        "content": openapi:mediaType-object($function)
      }
    }
 }
};

(:~
 : Prepare OAS3 Parameter Object.
 : @see https://swagger.io/specification/#mediaTypeObject
 :  :)
declare %private function openapi:parameter-object($function as element(function))
Mathias Goebel's avatar
Mathias Goebel committed
as map(*) {
    map{
      "parameters": array{
          openapi:parameters-path($function),
          openapi:parameters-query($function)
      }
    }
};

(:~
 : Prepares all PATH parameters for a given function
 : @param $function A function element from the inspect module
 : :)
declare %private function openapi:parameters-path($function as element(function))
as map(*)* {
Mathias Goebel's avatar
Mathias Goebel committed
    let $pathParameters :=
        $function/annotation[@name = "rest:path"][1]
            /tokenize(value, "\{")
            [starts-with(., "$")]
            ! (.
                => substring-after("$")
                => substring-before("}")
            )

    for $parameter in $pathParameters
    let $name := replace($parameter, "\{|\$|\}", "")
    let $argument := $function/argument[@var eq $name]
    let $basics := map:merge((
                map{
                    "name": $name,
                    "in": "path",
                    "required": true()},
                    openapi:schema-object($argument)
    ))
    let $description := $function/argument[@var = $name]/text() ! map{ "description": .}
    let $example := openapi:example($function, $name)
    return
        map:merge(($basics, $description, $example))

};

(:~
 : Prepares all QUERY parameters for a given function
 : @param $function A function element from the inspect module
 : :)
declare %private function openapi:parameters-query($function as element(function))
as map(*)* {
Mathias Goebel's avatar
Mathias Goebel committed
    let $queryParameters := $function/annotation[@name = "rest:query-param"]
    for $parameter in $queryParameters
    let $name := string($parameter/value[2]) => replace("\{|\$|\}", "")
    let $argument := $function/argument[@var eq $name]
    let $required := exists($parameter/value[3] and not(contains($argument/@cardinality, "zero")))
    let $basics :=
            map:merge((
                map{
                    "name": $name,
                    "in": "query",
                    "required": $required
                    },
                    openapi:schema-object($argument)
                ))
    let $description := $function/argument[@var = $name]/text() ! map{ "description": .}
    let $pos := index-of(($function/argument/string(@var)), $name)
    let $example := openapi:example($function, $name)
Mathias Goebel's avatar
Mathias Goebel committed
    return
        map:merge(($basics, $description, $example))
Mathias Goebel's avatar
Mathias Goebel committed
};

(:~
 : Prepare OAS3 Media Type Object.
 : @see https://swagger.io/specification/#mediaTypeObject
 :  :)
declare %private function openapi:mediaType-object($function)
Mathias Goebel's avatar
Mathias Goebel committed
as map(*) {
  let $produces := (
        string($function/annotation[@name="rest:produces"]),
        string($function/annotation[@name="output:media-type"]),
        string($function/annotation[@name="output:method"]/openapi:method-mediaType(string(.))),
        "application/xml"
      )
  return
    map{
      $produces[. != ""][1]: openapi:schema-object($function/returns)
    }
};

(:~
 : Prepare OAS3 Schema Object.
 : @param $returns A element from the inspect-module() function,
 : either *:returns or *:argument
 : @see https://swagger.io/specification/#mediaTypeObject
 :  :)
declare %private function openapi:schema-object($returns as element(*))
Mathias Goebel's avatar
Mathias Goebel committed
as map(*) {
  map{"schema":
    map:merge((
        map{
          "type": "string",
          "x-xml-type": string($returns/@type)
        },
        if(contains($returns/@cardinality, "zero")) then map{ "nullable": true() } else ()
    ))
  }
};

declare %private function openapi:tags-object($modules as element(module)+)
Mathias Goebel's avatar
Mathias Goebel committed
as map(*) {
  map{
    "tags": array{
        for $module in $modules
        return
            map{
                "name": string($module/@prefix),
                "description": normalize-space($module/description)
            }
        }
  }
};

(:~
 : Resolve an SPDX licenseId
 : @param a valid SPDX license code
 : @return a map with all SPDX data to the requested license
 :)
declare function openapi:spdx($licenseId as xs:string)
as map(*) {
let $collection-uri := /id("restxqopenapi")/base-uri()
let $item :=
 (($collection-uri || "/../spdx-licenses.json")
  => json-doc())("licenses")?*[?licenseId = $licenseId]

return
    map:merge($item)
};

(:~
 : Get a media type from a method call to XQuery Serialization
 : @param $method One of the specified methods
 : @see https://www.w3.org/TR/xslt-xquery-serialization/
 :  :)
declare %private function openapi:method-mediaType($method as xs:string?)
Mathias Goebel's avatar
Mathias Goebel committed
as xs:string?{
    switch ($method)
        case "html" return "text/html"
        case "text" return "text/plain"
        case "xml" return "application/xml"
        case "xhtml" return "application/xhtml+xml"
        case "json" return "application/json"
        (: case "adaptive" return () :)
        default return ()
};

(:~
 : Prepare an example value based on XQSuite annotation
 : @param $function A function element from inspect module
 : @param $name The name of the argument to prepare an example for :)
declare %private function openapi:example($function as element(function), $name as xs:string)
as map(*)* {
    string($function/annotation[@name = "test:arg"][value[1] = $name]/value[2])
    ! map{ "example": .}
};