Browse Source

model-server: rest api, swagger ui, response schemas, bug fixes

David Sehnal 5 years ago
parent
commit
f2966032d9

+ 8 - 0
src/servers/model/CHANGELOG.md

@@ -0,0 +1,8 @@
+# 0.9.0
+* REST API support.
+* Swagger UI support.
+* Response schemas.
+* Bug fixes.
+
+# 0.8.0
+* Let's call this an initial version.

+ 4 - 0
src/servers/model/query/schemas.ts

@@ -46,5 +46,9 @@ export const QuerySchemas = {
     interaction: <CifWriter.Category.Filter>{
         includeCategory(name) { return InteractionCategories.has(name); },
         includeField(cat, field) { return true; }
+    },
+    assembly: <CifWriter.Category.Filter>{
+        includeCategory(name) { return AssemblyCategories.has(name); },
+        includeField(cat, field) { return true; }
     }
 }

+ 89 - 0
src/servers/model/server/api-schema.ts

@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import VERSION from '../version'
+import { QueryParamInfo, QueryParamType, QueryDefinition, CommonQueryParamsInfo, QueryList } from './api';
+import ServerConfig from '../config';
+
+export const shortcutIconLink = `<link rel='shortcut icon' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAnUExURQAAAMIrHrspHr0oH7soILonHrwqH7onILsoHrsoH7soH7woILwpIKgVokoAAAAMdFJOUwAQHzNxWmBHS5XO6jdtAmoAAACZSURBVDjLxZNRCsQgDAVNXmwb9f7nXZEaLRgXloXOhwQdjMYYwpOLw55fBT46KhbOKhmRR2zLcFJQj8UR+HxFgArIF5BKJbEncC6NDEdI5SatBRSDJwGAoiFDONrEJXWYhGMIcRJGCrb1TOtDahfUuQXd10jkFYq0ViIrbUpNcVT6redeC1+b9tH2WLR93Sx2VCzkv/7NjfABxjQHksGB7lAAAAAASUVORK5CYII=' />`
+
+export function getApiSchema() {
+    return {
+        openapi: '3.0.0',
+        info: {
+            version: VERSION,
+            title: 'ModelServer',
+            description: 'The ModelServer is a service for accessing subsets of macromolecular model data.',
+        },
+        tags: [
+            {
+                name: 'General',
+            }
+        ],
+        paths: getPaths(),
+        components: {
+            parameters: {
+                id: {
+                    name: 'id',
+                    in: 'path',
+                    description: 'Id of the entry (i.e. 1tqn).',
+                    required: true,
+                    schema: {
+                        type: 'string',
+                    },
+                    style: 'simple'
+                },
+            }
+        }
+    }
+}
+
+function getPaths() {
+    const ret: any = {};
+    for (const { name, definition } of QueryList) {
+        ret[`${ServerConfig.appPrefix}/v1/{id}/${name}`] = getQueryInfo(definition);
+    }
+    return ret;
+}
+
+function getQueryInfo(def: QueryDefinition) {
+    return {
+        get: {
+            tags: ['General'],
+            summary: def.description,
+            operationId: def.name,
+            parameters: [
+                { $ref: '#/components/parameters/id' },
+                ...def.restParams.map(getParamInfo),
+                ...CommonQueryParamsInfo.map(getParamInfo)
+            ],
+            responses: {
+                200: {
+                    description: def.description,
+                    content: {
+                        'text/plain': {},
+                        'application/octet-stream': {},
+                    }
+                }
+            }
+        }
+    };
+}
+
+function getParamInfo(info: QueryParamInfo) {
+    return {
+        name: info.name,
+        in: 'query',
+        description: info.description,
+        required: !!info.required,
+        schema: {
+            type: info.type === QueryParamType.String ? 'string' : info.type === QueryParamType.Integer ? 'integer' : 'number',
+            enum: info.supportedValues ? info.supportedValues : void 0,
+            default: info.defaultValue
+        },
+        style: 'simple'
+    };
+}

+ 26 - 6
src/servers/model/server/api-web.ts

@@ -12,8 +12,9 @@ import { ConsoleLogger } from '../../../mol-util/console-logger';
 import { resolveJob } from './query';
 import { JobManager } from './jobs';
 import { UUID } from '../../../mol-util';
-import { LandingPage } from './landing';
 import { QueryDefinition, normalizeRestQueryParams, normalizeRestCommonParams, QueryList } from './api';
+import { getApiSchema, shortcutIconLink } from './api-schema';
+import { swaggerUiAssetsHandler, swaggerUiIndexHandler } from '../../common/swagger-ui';
 
 function makePath(p: string) {
     return Config.appPrefix + '/' + p;
@@ -85,8 +86,8 @@ async function processNextJob() {
 }
 
 function mapQuery(app: express.Express, queryName: string, queryDefinition: QueryDefinition) {
-    app.get(makePath('api/v1/:id/' + queryName), (req, res) => {
-        console.log({ queryName, params: req.params, query: req.query });
+    app.get(makePath('v1/:id/' + queryName), (req, res) => {
+        // console.log({ queryName, params: req.params, query: req.query });
         const entryId = req.params.id;
         const queryParams = normalizeRestQueryParams(queryDefinition, req.query);
         const commonParams = normalizeRestCommonParams(req.query);
@@ -131,7 +132,7 @@ export function initWebApi(app: express.Express) {
         });
     })
 
-    // app.get(makePath('api/v1/json'), (req, res) => {
+    // app.get(makePath('v1/json'), (req, res) => {
     //     const query = /\?(.*)$/.exec(req.url)![1];
     //     const args = JSON.parse(decodeURIComponent(query));
     //     const name = args.name;
@@ -152,7 +153,26 @@ export function initWebApi(app: express.Express) {
         mapQuery(app, q.name, q.definition);
     }
 
-    app.get('*', (req, res) => {
-        res.send(LandingPage);
+    const schema = getApiSchema();
+
+    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(schema));
     });
+
+    app.use(makePath(''), swaggerUiAssetsHandler());
+    app.get(makePath(''), swaggerUiIndexHandler({
+        openapiJsonUrl: makePath('openapi.json'),
+        apiPrefix: Config.appPrefix,
+        title: 'ModelServer API',
+        shortcutIconLink
+    }));
+
+    // app.get('*', (req, res) => {
+    //     res.send(LandingPage);
+    // });
 }

+ 5 - 4
src/servers/model/server/api.ts

@@ -13,8 +13,7 @@ export enum QueryParamType {
     JSON,
     String,
     Integer,
-    Float,
-    Boolean
+    Float
 }
 
 export interface QueryParamInfo<T extends string | number = string | number> {
@@ -139,7 +138,8 @@ const QueryMap = {
         structureTransform(p, s) {
             return StructureSymmetry.builderSymmetryMates(s, p.radius).run();
         },
-        jsonParams: [ RadiusParam ]
+        jsonParams: [ RadiusParam ],
+        filter: QuerySchemas.assembly
     }),
     'assembly': Q<{ name: string }>({
         niceName: 'Assembly',
@@ -154,7 +154,8 @@ const QueryMap = {
             defaultValue: '1',
             exampleValues: ['1'],
             description: 'Assembly name.'
-        }]
+        }],
+        filter: QuerySchemas.assembly
     }),
     'residueInteraction': Q<AtomSiteSchema & { radius: number }>({
         niceName: 'Residue Interaction',