ソースを参照

openapi schema and swaggerui for volume-server

Alexander Rose 6 年 前
コミット
274e51450e

+ 23 - 12
package-lock.json

@@ -450,6 +450,12 @@
         "@types/mime": "*"
       }
     },
+    "@types/swagger-ui-dist": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/swagger-ui-dist/-/swagger-ui-dist-3.0.0.tgz",
+      "integrity": "sha512-b0U4mJ01hpzfqCGOJJu9nPfwE9d/dwqdsrEXNKE5+GkvcMViaSPlAIfD/Hb8oGive4zq7KtTqpE5FD8RhxQ8TA==",
+      "dev": true
+    },
     "@types/valid-url": {
       "version": "1.0.2",
       "resolved": "http://registry.npmjs.org/@types/valid-url/-/valid-url-1.0.2.tgz",
@@ -1427,7 +1433,7 @@
     },
     "browserify-aes": {
       "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+      "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
       "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
       "dev": true,
       "requires": {
@@ -1472,7 +1478,7 @@
     },
     "browserify-rsa": {
       "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+      "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
       "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
       "dev": true,
       "requires": {
@@ -1524,7 +1530,7 @@
     },
     "buffer": {
       "version": "4.9.1",
-      "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
+      "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
       "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
       "dev": true,
       "requires": {
@@ -2514,7 +2520,7 @@
     },
     "create-hash": {
       "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+      "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
       "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
       "dev": true,
       "requires": {
@@ -2527,7 +2533,7 @@
     },
     "create-hmac": {
       "version": "1.1.7",
-      "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+      "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
       "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
       "dev": true,
       "requires": {
@@ -3005,7 +3011,7 @@
     },
     "diffie-hellman": {
       "version": "5.0.3",
-      "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+      "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
       "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
       "dev": true,
       "requires": {
@@ -11672,7 +11678,7 @@
       "dependencies": {
         "convert-source-map": {
           "version": "0.3.5",
-          "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz",
+          "resolved": "http://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz",
           "integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=",
           "dev": true
         }
@@ -12059,7 +12065,7 @@
         },
         "minimist": {
           "version": "1.2.0",
-          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
           "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
           "dev": true
         }
@@ -12359,7 +12365,7 @@
     },
     "sha.js": {
       "version": "2.4.11",
-      "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+      "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
       "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
       "dev": true,
       "requires": {
@@ -12812,7 +12818,7 @@
         },
         "readable-stream": {
           "version": "2.3.6",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+          "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
           "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
           "dev": true,
           "requires": {
@@ -12991,6 +12997,11 @@
         "has-flag": "^3.0.0"
       }
     },
+    "swagger-ui-dist": {
+      "version": "3.20.9",
+      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.20.9.tgz",
+      "integrity": "sha512-tlDVMtwpvA0QPxyda2paAPVR5UDNZI77LaEn/EZdhHLbmvBHACU3j6CTIV/wZSbc6qhpPdNrTB9dD3EDFue98Q=="
+    },
     "swap-case": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/swap-case/-/swap-case-1.1.2.tgz",
@@ -13404,7 +13415,7 @@
         },
         "minimist": {
           "version": "1.2.0",
-          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
           "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
           "dev": true
         },
@@ -13851,7 +13862,7 @@
       "dependencies": {
         "minimist": {
           "version": "1.2.0",
-          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
           "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
           "dev": true
         }

+ 3 - 1
package.json

@@ -85,6 +85,7 @@
     "@types/react": "^16.8.4",
     "@types/react-dom": "^16.8.2",
     "@types/webgl2": "0.0.4",
+    "@types/swagger-ui-dist": "3.0.0",
     "benchmark": "^2.1.4",
     "circular-dependency-plugin": "^5.0.2",
     "concurrently": "^4.1.0",
@@ -123,6 +124,7 @@
     "node-fetch": "^2.3.0",
     "react": "^16.8.2",
     "react-dom": "^16.8.2",
-    "rxjs": "^6.4.0"
+    "rxjs": "^6.4.0",
+    "swagger-ui-dist": "^3.20.9"
   }
 }

+ 30 - 0
src/servers/common/swagger-ui/index.ts

@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as express from 'express'
+import * as fs from 'fs'
+import { getAbsoluteFSPath } from 'swagger-ui-dist'
+import { ServeStaticOptions } from 'serve-static';
+import { interpolate } from 'mol-util/string';
+
+export function swaggerUiAssetsHandler(options?: ServeStaticOptions) {
+    const opts = options || {}
+    opts.index = false
+    return express.static(getAbsoluteFSPath(), opts)
+}
+
+function createHTML(swaggerUrl: string, apiPrefix: string) {
+    const htmlTemplate = fs.readFileSync(`${__dirname}/indexTemplate.html`).toString()
+    return interpolate(htmlTemplate, { swaggerUrl, apiPrefix })
+}
+
+export function swaggerUiIndexHandler(swaggerUrl: string, apiPrefix: string): express.Handler {
+    const html = createHTML(swaggerUrl, apiPrefix)
+    return (req: express.Request, res: express.Response) => {
+        res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
+        res.end(html);
+    }
+}

+ 64 - 0
src/servers/common/swagger-ui/indexTemplate.html

@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="UTF-8">
+        <title>Swagger UI</title>
+        <link rel="stylesheet" type="text/css" href="${apiPrefix}/swagger-ui.css" >
+        <style>
+            html
+            {
+                box-sizing: border-box;
+                overflow: -moz-scrollbars-vertical;
+                overflow-y: scroll;
+            }
+            *,
+            *:before,
+            *:after
+            {
+                box-sizing: inherit;
+            }
+            body
+            {
+                margin:0;
+                background: #fafafa;
+            }
+        </style>
+    </head>
+
+    <body>
+        <div id="swagger-ui"></div>
+
+        <script src="${apiPrefix}/swagger-ui-bundle.js"> </script>
+        <script src="${apiPrefix}/swagger-ui-standalone-preset.js"> </script>
+        <script>
+            function HidePlugin() {
+                // this plugin overrides some components to return nothing
+                return {
+                    components: {
+                        Topbar: function () { return null },
+                        Models: function () { return null },
+                    }
+                }
+            }
+            window.onload = function () {
+                var ui = SwaggerUIBundle({
+                    url: '${swaggerUrl}',
+                    validatorUrl: null,
+                    docExpansion: 'list',
+                    dom_id: '#swagger-ui',
+                    deepLinking: true,
+                    presets: [
+                        SwaggerUIBundle.presets.apis,
+                        SwaggerUIStandalonePreset
+                    ],
+                    plugins: [
+                        SwaggerUIBundle.plugins.DownloadUrl,
+                        HidePlugin
+                    ],
+                    layout: 'StandaloneLayout'
+                })
+                window.ui = ui
+            }
+        </script>
+    </body>
+</html>

+ 1 - 1
src/servers/volume/common/data-format.ts

@@ -32,7 +32,7 @@ export interface Sampling {
     rate: number,
     valuesInfo: ValuesInfo[],
 
-    /** Number of samples along each axis, in axisOrder  */
+    /** Number of samples along each axis, in axisOrder */
     sampleCount: number[]
 }
 

+ 10 - 5
src/servers/volume/server/api.ts

@@ -26,16 +26,21 @@ export function getOutputFilename(source: string, id: string, { asBinary, box, d
     return `${n(source)}_${n(id)}-${boxInfo}_${det}.${asBinary ? 'bcif' : 'cif'}`;
 }
 
+export interface ExtendedHeader extends DataFormat.Header {
+    availablePrecisions: { precision: number, maxVoxels: number }[]
+    isAvailable: boolean
+}
+
 /** Reads the header and includes information about available detail levels */
-export async function getHeaderJson(filename: string | undefined, sourceId: string) {
+export async function getExtendedHeaderJson(filename: string | undefined, sourceId: string) {
     ConsoleLogger.log('Header', sourceId);
     try {
         if (!filename || !File.exists(filename)) {
             ConsoleLogger.error(`Header ${sourceId}`, 'File not found.');
             return void 0;
         }
-        const header = { ...await readHeader(filename, sourceId) } as DataFormat.Header;
-        const { sampleCount } = header!.sampling[0];
+        const header: Partial<ExtendedHeader> = { ...await readHeader(filename, sourceId) };
+        const { sampleCount } = header.sampling![0];
         const maxVoxelCount = sampleCount[0] * sampleCount[1] * sampleCount[2];
         const precisions = LimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel
             .map((maxVoxels, precision) => ({ precision, maxVoxels }));
@@ -44,8 +49,8 @@ export async function getHeaderJson(filename: string | undefined, sourceId: stri
             availablePrecisions.push(p);
             if (p.maxVoxels > maxVoxelCount) break;
         }
-        (header as any).availablePrecisions = availablePrecisions;
-        (header as any).isAvailable = true;
+        header.availablePrecisions = availablePrecisions;
+        header.isAvailable = true;
         return JSON.stringify(header, null, 2);
     } catch (e) {
         ConsoleLogger.error(`Header ${sourceId}`, e);

+ 0 - 177
src/servers/volume/server/documentation.ts

@@ -1,177 +0,0 @@
-/**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * Taken/adapted from DensityServer (https://github.com/dsehnal/DensityServer)
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import VERSION from './version'
-import { LimitsConfig } from '../config';
-
-export function getDocumentation() {
-    function detail(i: number) {
-       return `<span class='id'>${i}</span><small> (${Math.round(100 *      LimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel[i] / 1000 / 1000) / 100 }M voxels)</small>`;
-    }
-    const detailMax = LimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel.length - 1;
-
-    // TODO get from config
-    const dataSource = `Specifies the data source (determined by the experiment method). Currently, <span class='id'>x-ray</span> and <span class='id'>em</span> sources are supported.`;
-    const entryId = `Id of the entry. For <span class='id'>x-ray</span>, use PDB ID (i.e. <span class='id'>1cbs</span>) and for <span class='id'>em</span> use EMDB id (i.e. <span class='id'>emd-8116</span>).`;
-
-    return `<!DOCTYPE html>
-<html xmlns="http://www.w3.org/1999/xhtml">
-<head>
-<meta charset="utf-8" />
-<link rel='shortcut icon' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAnUExURQAAAMIrHrspHr0oH7soILonHrwqH7onILsoHrsoH7soH7woILwpIKgVokoAAAAMdFJOUwAQHzNxWmBHS5XO6jdtAmoAAACZSURBVDjLxZNRCsQgDAVNXmwb9f7nXZEaLRgXloXOhwQdjMYYwpOLw55fBT46KhbOKhmRR2zLcFJQj8UR+HxFgArIF5BKJbEncC6NDEdI5SatBRSDJwGAoiFDONrEJXWYhGMIcRJGCrb1TOtDahfUuQXd10jkFYq0ViIrbUpNcVT6redeC1+b9tH2WLR93Sx2VCzkv/7NjfABxjQHksGB7lAAAAAASUVORK5CYII=' />
-<title>VolumeServer (${VERSION})</title>
-<style>
-html { -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; }
-body { margin: 0; font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; font-weight: 300; color: #333; line-height: 1.42857143; font-size: 14px }
-.container { padding: 0 15px; max-width: 970px; margin: 0 auto; }
-small { font-size: 80% }
-h2, h4 { font-weight: 500; line-height: 1.1; }
-h2 { color: black; font-size: 24px; }
-h4 { font-size: 18px; margin: 20px 0 10px 0 }
-h2 small { color: #777; font-weight: 300 }
-hr { box-sizing: content-box; height: 0; overflow: visible; }
-a { background-color: transparent; -webkit-text-decoration-skip: objects; text-decoration: none }
-a:active, a:hover { outline-width: 0; }
-a:focus, a:hover { text-decoration: underline; color: #23527c }
-.list-unstyled { padding: 0; list-style: none; margin: 0 0 10px 0 }
-.cs-docs-query-wrap { padding: 24px 0; border-bottom: 1px solid #eee }
-.cs-docs-query-wrap > h2 { margin: 0; color: black; }
-.cs-docs-query-wrap > h2 > span { color: #DE4D4E; font-family: Menlo,Monaco,Consolas,"Courier New",monospace; font-size: 90% }
-.cs-docs-param-name, .cs-docs-template-link { color: #DE4D4E; font-family: Menlo,Monaco,Consolas,"Courier New",monospace }
-table {margin: 0; padding: 0; }
-table th { font-weight: bold; border-bottom: none; text-align: left; padding: 6px 12px }
-td { padding: 6px 12px }
-td:not(:last-child), th:not(:last-child) { border-right: 1px dotted #ccc }
-tr:nth-child(even) { background: #f9f9f9 }
-span.id  { color: #DE4D4E; font-family: Menlo,Monaco,Consolas,"Courier New",monospace; }
-</style>
-</head>
-<body>
-<div class="container">
-<div style='text-align: center; margin-top: 24px;'><span style='font-weight: bold; font-size: 16pt'>VolumeServer</span> <span>${VERSION}</span></div>
-
-<div style='text-align: justify; padding: 24px 0; border-bottom: 1px solid #eee'>
-  <p>
-    <b>VolumeServer</b> is a service for accessing subsets of volumetric density data. It automatically downsamples the data
-    depending on the volume of the requested region to reduce the bandwidth requirements and provide near-instant access to even the
-    largest data sets.
-  </p>
-  <p>
-    It uses the text based <a href='https://en.wikipedia.org/wiki/Crystallographic_Information_File'>CIF</a> and binary
-    <a href='https://github.com/dsehnal/BinaryCIF' style='font-weight: bold'>BinaryCIF</a>
-    formats to deliver the data to the client.
-    The server support is integrated into the <a href='https://github.com/dsehnal/LiteMol' style='font-weight: bold'>LiteMol Viewer</a>.
-  </p>
-</div>
-
-<div class="cs-docs-query-wrap">
-  <h2>Data Header / Check Availability <span>/&lt;source&gt;/&lt;id&gt;</span><br>
-  <small>Returns a JSON response specifying if data is available and the maximum region that can be queried.</small></h2>
-  <div id="coordserver-documentation-ambientResidues-body" style="margin: 24px 24px 0 24px">
-    <h4>Examples</h4>
-    <a href="/VolumeServer/x-ray/1cbs" class="cs-docs-template-link" target="_blank" rel="nofollow">/x-ray/1cbs</a><br>
-    <a href="/VolumeServer/em/emd-8116" class="cs-docs-template-link" target="_blank" rel="nofollow">/em/emd-8116</a>
-    <h4>Parameters</h4>
-    <table cellpadding="0" cellspacing="0" style='width: 100%'>
-    <tbody><tr><th style='width: 80px'>Name</th><th>Description</th></tr>
-    <tr>
-    <td class="cs-docs-param-name">source</td>
-    <td>${dataSource}</td>
-    </tr>
-    <tr>
-    <td class="cs-docs-param-name">id</td>
-    <td>${entryId}</td>
-    </tr>
-    </tbody></table>
-  </div>
-</div>
-
-<div class="cs-docs-query-wrap">
-  <h2>Box <span>/&lt;source&gt;/&lt;id&gt;/box/&lt;a,b,c&gt;/&lt;u,v,w&gt;?&lt;optional parameters&gt;</span><br>
-  <small>Returns density data inside the specified box for the given entry. For X-ray data, returns 2Fo-Fc and Fo-Fc volumes in a single response.</small></h2>
-  <div style="margin: 24px 24px 0 24px">
-    <h4>Examples</h4>
-    <a href="/VolumeServer/em/emd-8003/box/-2,7,10/4,10,15.5?encoding=cif&space=cartesian" class="cs-docs-template-link" target="_blank" rel="nofollow">/em/emd-8003/box/-2,7,10/4,10,15.5?excoding=cif&space=cartesian</a><br>
-    <a href="/VolumeServer/x-ray/1cbs/box/0.1,0.1,0.1/0.23,0.31,0.18?space=fractional" class="cs-docs-template-link" target="_blank" rel="nofollow">/x-ray/1cbs/box/0.1,0.1,0.1/0.23,0.31,0.18?space=fractional</a>
-    <h4>Parameters</h4>
-    <table cellpadding="0" cellspacing="0" style='width: 100%'>
-    <tbody><tr><th style='width: 80px'>Name</th><th>Description</th></tr>
-    <tr>
-    <td class="cs-docs-param-name">source</td>
-    <td>${dataSource}</td>
-    </tr>
-    <tr>
-    <td class="cs-docs-param-name">id</td>
-    <td>${entryId}</td>
-    </tr>
-    <tr>
-    <td class="cs-docs-param-name">a,b,c</td>
-    <td>Bottom left corner of the query region in Cartesian or fractional coordinates (determined by the <span class='id'>&amp;space</span> query parameter).</td>
-    </tr>
-    <tr>
-    <td class="cs-docs-param-name">u,v,w</td>
-    <td>Top right corner of the query region in Cartesian or fractional coordinates (determined by the <span class='id'>&amp;space</span> query parameter).</td>
-    </tr>
-    <tr>
-    <td class="cs-docs-param-name">encoding</td>
-    <td>Determines if text based <span class='id'>CIF</span> or binary <span class='id'>BinaryCIF</span> encoding is used. An optional argument, default is <span class='id'>BinaryCIF</span> encoding.</td>
-    </tr>
-    <tr>
-    <td class="cs-docs-param-name">space</td>
-    <td>Determines the coordinate space the query is in. Can be <span class='id'>cartesian</span> or <span class='id'>fractional</span>. An optional argument, default values is <span class='id'>cartesian</span>.</td>
-    </tr>
-    <tr>
-      <td class="cs-docs-param-name">detail</td>
-      <td>
-        Determines the maximum number of voxels the query can return. Possible values are in the range from ${detail(0)} to ${detail(detailMax)}.
-        Default value is <span class='id'>0</span>. Note: different detail levels might lead to the same result.
-      </td>
-    </tr>
-    </tbody></table>
-  </div>
-</div>
-
-<div class="cs-docs-query-wrap">
-  <h2>Cell <span>/&lt;source&gt;/&lt;id&gt;/cell?&lt;optional parameters&gt;</span><br>
-  <small>Returns (downsampled) volume data for the entire "data cell". For X-ray data, returns unit cell of 2Fo-Fc and Fo-Fc volumes, for EM data returns everything.</small></h2>
-  <div style="margin: 24px 24px 0 24px">
-    <h4>Example</h4>
-    <a href="/VolumeServer/em/emd-8116/cell?detail=1" class="cs-docs-template-link" target="_blank" rel="nofollow">/em/emd-8116/cell?detail=1</a><br>
-    <h4>Parameters</h4>
-    <table cellpadding="0" cellspacing="0" style='width: 100%'>
-    <tbody><tr><th style='width: 80px'>Name</th><th>Description</th></tr>
-    <tr>
-    <td class="cs-docs-param-name">source</td>
-    <td>${dataSource}</td>
-    </tr>
-    <tr>
-    <td class="cs-docs-param-name">id</td>
-    <td>${entryId}</td>
-    </tr>
-    <tr>
-    <td class="cs-docs-param-name">encoding</td>
-    <td>Determines if text based <span class='id'>CIF</span> or binary <span class='id'>BinaryCIF</span> encoding is used. An optional argument, default is <span class='id'>BinaryCIF</span> encoding.</td>
-    </tr>
-    <tr>
-      <td class="cs-docs-param-name">detail</td>
-      <td>
-        Determines the maximum number of voxels the query can return. Possible values are in the range from ${detail(0)} to ${detail(detailMax)}.
-        Default value is <span class='id'>0</span>. Note: different detail levels might lead to the same result.
-      </td>
-    </tr>
-    </tbody></table>
-  </div>
-</div>
-
-
-<div style="color: #999;font-size:smaller;margin: 20px 0; text-align: right">&copy; 2016 &ndash; now, David Sehnal | Node ${process.version}</div>
-
-</body>
-</html>`;
-}

+ 14 - 7
src/servers/volume/server/web-api.ts

@@ -10,19 +10,19 @@
 import * as express from 'express'
 
 import * as Api from './api'
-
 import * as Data from './query/data-model'
 import * as Coords from './algebra/coordinate'
-import { getDocumentation } from './documentation'
 import { ConsoleLogger } from 'mol-util/console-logger'
 import { State } from './state'
 import { LimitsConfig, ServerConfig } from '../config';
 import { interpolate } from 'mol-util/string';
+import { getSchema } from './web-schema';
+import { swaggerUiIndexHandler, swaggerUiAssetsHandler } from 'servers/common/swagger-ui';
 
 export default function init(app: express.Express) {
     app.locals.mapFile = getMapFileFn()
     function makePath(p: string) {
-        return ServerConfig.apiPrefix + '/' + p;
+        return `${ServerConfig.apiPrefix}/${p}`;
     }
 
     // Header
@@ -32,10 +32,17 @@ export default function init(app: express.Express) {
     // Cell /:src/:id/cell/?text=0|1&space=cartesian|fractional
     app.get(makePath(':source/:id/cell/?'), (req, res) => queryBox(req, res, getQueryParams(req, true)));
 
-    app.get('*', (req, res) => {
-        res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
-        res.end(getDocumentation());
+    app.get(makePath('openapi.json'), (req, res) => {
+        res.writeHead(200, {
+            'Content-Type': 'application/json; charset=utf-8',
+            'Access-Control-Allow-Origin': '*',
+            'Access-Control-Allow-Headers': 'X-Requested-With'
+        });
+        res.end(JSON.stringify(getSchema()));
     });
+
+    app.use(makePath(''), swaggerUiAssetsHandler());
+    app.get(makePath(''), swaggerUiIndexHandler(makePath('openapi.json'), ServerConfig.apiPrefix));
 }
 
 function getMapFileFn() {
@@ -115,7 +122,7 @@ async function getHeader(req: express.Request, res: express.Response) {
 
     try {
         const { filename, id } = getSourceInfo(req);
-        const header = await Api.getHeaderJson(filename, id);
+        const header = await Api.getExtendedHeaderJson(filename, id);
         if (!header) {
             res.writeHead(404);
             return;

+ 259 - 0
src/servers/volume/server/web-schema.ts

@@ -0,0 +1,259 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import VERSION from './version'
+import { LimitsConfig, ServerConfig } from '../config';
+
+export function getSchema() {
+    function detail(i: number) {
+       return `${i} (${Math.round(100 * LimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel[i] / 1000 / 1000) / 100 }M voxels)`;
+    }
+    const detailMax = LimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel.length - 1;
+    const sources = ServerConfig.idMap.map(m => m[0])
+
+    return {
+        openapi: '3.0.0',
+        info: {
+            version: VERSION,
+            title: 'Volume Server',
+            description: 'The VolumeServer is a service for accessing subsets of volumetric data. It automatically downsamples the data depending on the volume of the requested region to reduce the bandwidth requirements and provide near-instant access to even the largest data sets.',
+        },
+        tags: [
+            {
+                name: 'General',
+            }
+        ],
+        paths: {
+            [`${ServerConfig.apiPrefix}/{source}/{id}/`]: {
+                get: {
+                    tags: ['General'],
+                    summary: 'Returns a JSON response specifying if data is available and the maximum region that can be queried.',
+                    operationId: 'getInfo',
+                    parameters: [
+                        { $ref: '#/components/parameters/source' },
+                        { $ref: '#/components/parameters/id' },
+                    ],
+                    responses: {
+                        200: {
+                            description: 'Volume availability and info',
+                            content: {
+                                'application/json': {
+                                    schema: { $ref: '#/components/schemas/info' }
+                                }
+                            }
+                        },
+                    },
+                }
+            },
+            [`${ServerConfig.apiPrefix}/{source}/{id}/box/{a1,a2,a3}/{b1,b2,b3}/`]: {
+                get: {
+                    tags: ['General'],
+                    summary: 'Returns density data inside the specified box for the given entry. For X-ray data, returns 2Fo-Fc and Fo-Fc volumes in a single response.',
+                    operationId: 'getBox',
+                    parameters: [
+                        { $ref: '#/components/parameters/source' },
+                        { $ref: '#/components/parameters/id' },
+                        {
+                            name: 'bottomLeftCorner',
+                            in: 'path',
+                            description: 'Bottom left corner of the query region in Cartesian or fractional coordinates (determined by the `space` query parameter).',
+                            required: true,
+                            schema: {
+                                type: 'list',
+                                items: {
+                                    type: 'float',
+                                }
+                            },
+                            style: 'simple'
+                        },
+                        {
+                            name: 'topRightCorner',
+                            in: 'path',
+                            description: 'Top right corner of the query region in Cartesian or fractional coordinates (determined by the `space` query parameter).',
+                            required: true,
+                            schema: {
+                                type: 'list',
+                                items: {
+                                    type: 'float',
+                                }
+                            },
+                            style: 'simple'
+                        },
+                        { $ref: '#/components/parameters/encoding' },
+                        { $ref: '#/components/parameters/detail' },
+                        {
+                            name: 'space',
+                            in: 'query',
+                            description: 'Determines the coordinate space the query is in. Can be cartesian or fractional. An optional argument, default values is cartesian.',
+                            schema: {
+                                type: 'string',
+                                enum: ['cartesian', 'fractional']
+                            },
+                            style: 'form'
+                        }
+                    ],
+                    responses: {
+                        200: {
+                            description: 'Volume box',
+                            content: {
+                                'text/plain': {},
+                                'application/octet-stream': {},
+                            }
+                        },
+                    },
+                }
+            },
+            [`${ServerConfig.apiPrefix}/{source}/{id}/cell/`]: {
+                get: {
+                    tags: ['General'],
+                    summary: 'Returns (downsampled) volume data for the entire "data cell". For X-ray data, returns unit cell of 2Fo-Fc and Fo-Fc volumes, for EM data returns everything.',
+                    operationId: 'getCell',
+                    parameters: [
+                        { $ref: '#/components/parameters/source' },
+                        { $ref: '#/components/parameters/id' },
+                        { $ref: '#/components/parameters/encoding' },
+                        { $ref: '#/components/parameters/detail' },
+                    ],
+                    responses: {
+                        200: {
+                            description: 'Volume cell',
+                            content: {
+                                'text/plain': {},
+                                'application/octet-stream': {},
+                            }
+                        },
+                    },
+                }
+            }
+        },
+        components: {
+            schemas: {
+                // TODO how to keep in sync with (or derive from) `api.ts/ExtendedHeader`
+                info: {
+                    properties: {
+                        formatVersion: {
+                            type: 'string',
+                            description: 'Format version number'
+                        },
+                        axisOrder: {
+                            type: 'array',
+                            items: { type: 'number' },
+                            description: 'Axis order from the slowest to fastest moving, same as in CCP4'
+                        },
+                        origin: {
+                            type: 'array',
+                            items: { type: 'number' },
+                            description: 'Origin in fractional coordinates, in axisOrder'
+                        },
+                        dimensions: {
+                            type: 'array',
+                            items: { type: 'number' },
+                            description: 'Dimensions in fractional coordinates, in axisOrder'
+                        },
+                        spacegroup: {
+                            properties: {
+                                number: { type: 'number' },
+                                size: {
+                                    type: 'array',
+                                    items: { type: 'number' }
+                                },
+                                angles: {
+                                    type: 'array',
+                                    items: { type: 'number' }
+                                },
+                                isPeriodic: {
+                                    type: 'boolean',
+                                    description: 'Determine if the data should be treated as periodic or not. (e.g. X-ray = periodic, EM = not periodic)'
+                                },
+                            }
+                        },
+                        channels: {
+                            type: 'array',
+                            items: { type: 'string' }
+                        },
+                        valueType: {
+                            type: 'string',
+                            enum: ['float32', 'int16', 'int8'],
+                            description: 'Determines the data type of the values'
+                        },
+                        blockSize: {
+                            type: 'number',
+                            description: 'The value are stored in blockSize^3 cubes'
+                        },
+                        sampling: {
+                            type: 'array',
+                            items: {
+                                properties: {
+                                    byteOffset: { type: 'number' },
+                                    rate: {
+                                        type: 'number',
+                                        description: 'How many values along each axis were collapsed into 1'
+                                    },
+                                    valuesInfo: {
+                                        properties: {
+                                            mean: { type: 'number' },
+                                            sigma: { type: 'number' },
+                                            min: { type: 'number' },
+                                            max: { type: 'number' },
+                                        }
+                                    },
+                                    sampleCount: {
+                                        type: 'array',
+                                        items: { type: 'number' },
+                                        description: 'Number of samples along each axis, in axisOrder'
+                                    },
+                                }
+                            }
+                        }
+                    }
+                }
+            },
+            parameters: {
+                source: {
+                    name: 'source',
+                    in: 'path',
+                    description: `Specifies the data source (determined by the experiment method). Currently supported sources are: ${sources.join(', ')}.`,
+                    required: true,
+                    schema: {
+                        type: 'string',
+                        enum: sources
+                    },
+                    style: 'simple'
+                },
+                id: {
+                    name: 'id',
+                    in: 'path',
+                    description: 'Id of the entry. For x-ray, use PDB ID (i.e. 1cbs) and for em use EMDB id (i.e. emd-8116).',
+                    required: true,
+                    schema: {
+                        type: 'string',
+                    },
+                    style: 'simple'
+                },
+                encoding: {
+                    name: 'encoding',
+                    in: 'query',
+                    description: 'Determines if text based CIF or binary BinaryCIF encoding is used. An optional argument, default is BinaryCIF encoding.',
+                    schema: {
+                        type: 'string',
+                        enum: ['cif', 'bcif']
+                    },
+                    style: 'form'
+                },
+                detail: {
+                    name: 'detail',
+                    in: 'query',
+                    description: `Determines the maximum number of voxels the query can return. Possible values are in the range from ${detail(0)} to ${detail(detailMax)}. Default value is 0. Note: different detail levels might lead to the same result.`,
+                    schema: {
+                        type: 'integer',
+                    },
+                    style: 'form'
+                }
+            }
+        }
+    }
+}