Gitlab Community Edition Instance

Skip to content
Snippets Groups Projects
openapi.xqm 15.4 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 conform to OpenAPI 3.0.2, usually to be stored as "openapi.json".
Mathias Goebel's avatar
Mathias Goebel committed
 : @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 := openapi:files($target, ())[openapi:xquery-resource(.)]
Mathias Goebel's avatar
Mathias Goebel committed
  let $module :=
    for $module in $modules-uris
    let $test4rest := contains(util:binary-doc($module) => util:base64-decode(), "%rest:")
    where $test4rest
Mathias Goebel's avatar
Mathias Goebel committed
    return
      inspect:inspect-module($module)[.//annotation[@name = $openapi:supported-methods]]
Mathias Goebel's avatar
Mathias Goebel committed

  let $config-uri := $target || "/openapi-config.xml"
Mathias Goebel's avatar
Mathias Goebel committed
  let $config :=  if(doc-available($config-uri))
                  then doc($config-uri)/*
                  else doc( replace(system:get-module-load-path(), '^(xmldb:exist://)?(embedded-eXist-server)?(.+)$', '$3') || "/../openapi-config.xml" )/*
Mathias Goebel's avatar
Mathias Goebel committed
  let $expath := doc($target || "/expath-pkg.xml")/*
Mathias Goebel's avatar
Mathias Goebel committed
  let $repo := doc($target || "/repo.xml")/*
Mathias Goebel's avatar
Mathias Goebel committed
  return
    map:merge((
    map{"openapi": "3.0.2"},
Mathias Goebel's avatar
Mathias Goebel committed
    openapi:paths-object($module, $config),
Mathias Goebel's avatar
Mathias Goebel committed
    openapi:servers-object($config/openapi:servers),
    openapi:info-object($expath, $repo, $config/openapi:info),
Mathias Goebel's avatar
Mathias Goebel committed
    openapi:tags-object($module, $config)
Mathias Goebel's avatar
Mathias Goebel committed
    ))
};

(:~
 : 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{
Mathias Goebel's avatar
Mathias Goebel committed
        for $server in $config/openapi:server
        return
          map{
            "url": string($server/@url),
            "description": string($server)
          }
Mathias Goebel's avatar
Mathias Goebel committed
  }
};

(:~
 : Prepare OAS3 Paths Object.
 : @see https://swagger.io/specification/#pathsObject
 :)
Mathias Goebel's avatar
Mathias Goebel committed
declare %private function openapi:paths-object($module as element(module)+, $config as element(openapi:config))
Mathias Goebel's avatar
Mathias Goebel committed
as map(*) {
  let $paths := $module/function/annotation[@name = "rest:path"]/value => distinct-values()
  return
Mathias Goebel's avatar
Mathias Goebel committed
  map{
    "paths":
      map:merge((
        for $path in $paths
        let $functions := $module/function[annotation[@name = "rest:path"]/value = $path]
        return
          map{
              $path => replace("\{\$", "{"):
Mathias Goebel's avatar
Mathias Goebel committed
                map:merge(($functions ! openapi:operation-object(., $config)))
Mathias Goebel's avatar
Mathias Goebel committed
      ))
  }
};

(:~
 : Prepare OAS3 Operation Object.
 : @see https://swagger.io/specification/#operationObject
 :)
Mathias Goebel's avatar
Mathias Goebel committed
declare %private function openapi:operation-object($function as element(function), $config as element(openapi:config))
Mathias Goebel's avatar
Mathias Goebel committed
as map(*) {
Mathias Goebel's avatar
Mathias Goebel committed
  let $name := $function/@name
  let $desc := tokenize($function/description, "\n\s\n\s") ! normalize-space(.)
Mathias Goebel's avatar
Mathias Goebel committed
  let $see := normalize-space($function/see)
  let $deprecated := $function/deprecated
Mathias Goebel's avatar
Mathias Goebel committed
  let $tags := array {
      if($config/openapi:tags/openapi:tag/openapi:function[@name = $name])
      then
        if($config/openapi:tags/openapi:tag/openapi:function[@name = $name]/parent::openapi:tag/string(@method) = "exclusive")
        then $config/openapi:tags/openapi:tag/openapi:function[@name = $name]/parent::openapi:tag[@method = "exclusive"]/string(@name)
        else
            ($name => substring-before(":"),
            $config/openapi:tags/openapi:tag/openapi:function[@name = $name]/parent::openapi:tag/string(@name))
      else
        $name => substring-before(":")
  }
Mathias Goebel's avatar
Mathias Goebel committed
  return
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((
Mathias Goebel's avatar
Mathias Goebel committed
        map{ "summary": $desc[1]},
        $desc[2] ! map{ "description": .},
Mathias Goebel's avatar
Mathias Goebel committed
        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
      ))
    }
Mathias Goebel's avatar
Mathias Goebel committed
};

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 Responses Object.
Mathias Goebel's avatar
Mathias Goebel committed
 : @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/#parameterObject
Mathias Goebel's avatar
Mathias Goebel committed
 :  :)
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($function, "query"),
          openapi:parameters($function, "header"),
          openapi:parameters($function, "cookie")
      }
    }
};

(:~
 : Prepares all PATH parameters for a given function
 : @param $function A function element from the inspect module
 : @see https://swagger.io/specification/#parameterObject
 : :)
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, HEADER and COOKIE parameters for a given function
 : @param $function A function element from the inspect module
 : @see https://swagger.io/specification/#parameterObject
declare %private function openapi:parameters($function as element(function), $source as xs:string)
    let $parameters := $function/annotation[@name = "rest:" || $source || "-param"]
    for $annotation in $parameters
        let $varName := string($annotation/value[2]) => replace("\{|\$|\}", "")
        let $name := string($annotation/value[1])
        let $argument := $function/argument[@var eq $varName]
        let $required :=
            (: when a default value is present and the cardinality is not ? or * :)
            exists($annotation/value[3]) and not(contains($argument/@cardinality, "zero"))
        let $basics :=
                map:merge((
                    map{
                        "name": $name,
                        "in": $source,
                        "required": $required
                        },
                        openapi:schema-object($argument)
        let $description := $argument/text() ! map{ "description": .}
        let $example := openapi:example($function, $varName)
        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 := (
Mathias Goebel's avatar
Mathias Goebel committed
        $function/annotation[@name="rest:produces"]/string(value),
Mathias Goebel's avatar
Mathias Goebel committed
        string($function/annotation[@name="output:media-type"]),
        string($function/annotation[@name="output:method"]/openapi:method-mediaType(string(.))),
        "application/xml"
      )
  return
      map:merge(
          for $iii in 1 to count($produces) return
              if($produces[$iii] = "") then
                  ()
              else
                map:entry($produces[$iii], openapi:schema-object($function/returns))
      )
Mathias Goebel's avatar
Mathias Goebel committed
};

(:~
 : 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
    map:merge((
        map{
          "type": "string",
          "x-xml-type": string($returns/@type)
        },
        if(contains($returns/@cardinality, "zero"))
        then map{ "nullable": true() }
        else ()
Mathias Goebel's avatar
Mathias Goebel committed
declare %private function openapi:tags-object($modules as element(module)+, $config as element(openapi:config))
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)
Mathias Goebel's avatar
Mathias Goebel committed
            },
        for $tag in $config/openapi:tags/openapi:tag
        return
            map{
                "name": string($tag/@name),
                "description": normalize-space($tag)
Mathias Goebel's avatar
Mathias Goebel committed
            }
Mathias Goebel's avatar
Mathias Goebel committed
  }
};

(:~
 : 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] eq $name])[1]/value[2])

declare function openapi:xquery-resource($baseuri as xs:string)
as xs:boolean {
     ends-with($baseuri, ".xqm")
  or ends-with($baseuri, ".xql")
  or ends-with($baseuri, ".xq")
  or ends-with($baseuri, ".xquery")
  or ends-with($baseuri, ".xqy")
};

(:~
 : Returns the names of the child resources in collection URI supplied
 : @param $target URI under which child resources are to be returned :)
declare function openapi:files-uris($target as xs:string) {
	let $new-uris as xs:string* := xmldb:get-child-resources($target)
	let $new-uris as xs:string* :=
	    for $n in $new-uris return fn:concat($target, '/', $n)
	return $new-uris
};

(:~
 : Returns the names of the child collections in collection URI supplied
 : @param $target URI under which collections are to be returned :)
declare function openapi:collections-uris($target as xs:string) {
	let $collections as xs:string* := xmldb:get-child-collections($target)
	return $collections
};

(:~
 : Recursive function to use instead of collection as implementation of this function
 : varies between Fusion and eXist so in Fusion XQuery files are not found.
 : @param $target URI under which file listing is required
 : @param $uris Existing URIs to be retained and added to until completion :)
declare function openapi:files($target as xs:string, $uris as xs:string*) {
	let $current-uris as xs:string* := openapi:files-uris($target)
	let $current-collections as xs:string* := openapi:collections-uris($target)
	let $new-uris as xs:string* :=
    	if (fn:empty($current-collections)) then ()
    	else
        	for $c in $current-collections
        	    let $target := fn:concat($target, '/', $c)
            	return (openapi:files($target, $uris))
    let $uris as xs:string* := ($current-uris, $new-uris, $uris)
    return ($uris)