diff --git a/src/servers/model/CHANGELOG.md b/src/servers/model/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..d522b8879c7e0b8c5a12c8e86b44139757e06d7a --- /dev/null +++ b/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. \ No newline at end of file diff --git a/src/servers/model/query/schemas.ts b/src/servers/model/query/schemas.ts index 7f852bcae761380d94324eb0db8dcdac667cf509..d96013dc5b21837f39841c9bd0eb9413111b41d5 100644 --- a/src/servers/model/query/schemas.ts +++ b/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; } } } \ No newline at end of file diff --git a/src/servers/model/server/api-schema.ts b/src/servers/model/server/api-schema.ts new file mode 100644 index 0000000000000000000000000000000000000000..f22f874f1b7725b2b0338be8898e464986cd0c29 --- /dev/null +++ b/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' + }; +} \ No newline at end of file diff --git a/src/servers/model/server/api-web.ts b/src/servers/model/server/api-web.ts index 45cab67092243e4b7d635a60e974dca6113b730a..daf1c760bfdd0a6143dbbca45dd7b95305dcb11e 100644 --- a/src/servers/model/server/api-web.ts +++ b/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); + // }); } \ No newline at end of file diff --git a/src/servers/model/server/api.ts b/src/servers/model/server/api.ts index 86a6c36dda5fd7ca17f55ca56716855ccec15652..8069042b916e47ac12d61bde82df4ad2989fdf1e 100644 --- a/src/servers/model/server/api.ts +++ b/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',