Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OpenApi spec and Swagger documentation #131

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ It’s also possible to opt out of proxying for specific hosts by using the `NO_

## Endpoints

### Documentation

Swagger documentation is available at http://127.0.0.1:1969/?doc

### Web Translation

#### Retrieve metadata for a webpage:
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"aws-sdk": "^2.326.0",
"config": "^1.30.0",
"iconv-lite": "^0.4.24",
"js-yaml": "^4.1.0",
"jsdom": "^13.1.0",
"koa": "^2.13.4",
"koa-bodyparser": "^4.3.0",
Expand All @@ -20,6 +21,7 @@
"request": "^2.87.0",
"request-promise-native": "^1.0.5",
"serverless-http": "^2.7.0",
"swagger-ui-dist": "^3.52.3",
"wicked-good-xpath": "git+https://[email protected]/zotero/wicked-good-xpath.git#1b88459",
"xregexp": "^4.2.0",
"yargs": "^12.0.2"
Expand Down
142 changes: 142 additions & 0 deletions spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
openapi: 3.0.0
info:
version: 1.0.0
title: Zotero translation-server
description: The Zotero translation server lets you use Zotero translators without the Zotero client.
license:
name: AGPL-3.0-only
url: https://spdx.org/licenses/AGPL-3.0-only.html
paths:
/web:
post:
tags:
- Web
description: Retrieve metadata for a webpage
requestBody:
required: true
content:
text/plain:
schema:
type: string
example: http://www.example.com
produces:
- application/json; charset=utf-8;
responses:
'200':
description: Returns an array of translated items in Zotero API JSON format
schema:
type: Array
'300':
description: Multiple items found
schema:
type: Object
properties:
url:
type: string
session:
type: string
items:
type: Object
'400':
description: Remote page not found
schema:
type: string
/search:
post:
tags:
- Search
description: Retrieve metadata from an identifier (DOI, ISBN, PMID, arXiv ID)
requestBody:
required: true
content:
text/plain:
schema:
type: string
encoding:
allowReserved: true
produces:
- application/json; charset=utf-8;
responses:
'200':
description: The citation data in the requested format
schema:
type: Array
'501':
description: Metadata for identifier was not found
schema:
type: string
/export:
post:
tags:
- Export
description: Convert items in Zotero API JSON format to a supported export format (RIS, BibTeX, etc.)
requestBody:
required: true
content:
application/json:
schema:
type: array
parameters:
- name: format
in: query
description: Requested format to covert body to
schema:
type: string
enum:
- bibtex
- biblatex
- bookmarks
- coins
- csljson
- csv
- endnote_xml
- evernote
- mods
- rdf_bibliontology
- rdf_dc
- rdf_zotero
- refer
- refworks_tagged
- ris
- tei
- wikipedia
required: true
produces:
- application/json; charset=utf-8;
responses:
'200':
description: The citation data in the requested format
'415':
description: Unsupported media type
schema:
type: string
'501':
description: Failed to translate
schema:
type: string
/import:
post:
tags:
- Import
description: Convert items in any import format to the Zotero API JSON format
requestBody:
required: true
content:
text/plain:
schema:
type: string
produces:
- application/json; charset=utf-8;
responses:
'200':
description: The citation data in the requested format
schema:
type: array
'415':
description: Unsupported media type
schema:
type: string
'500':
description: No suitable translators found
schema:
type: string
2 changes: 2 additions & 0 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const SearchEndpoint = require('./searchEndpoint');
const WebEndpoint = require('./webEndpoint');
const ExportEndpoint = require('./exportEndpoint');
const ImportEndpoint = require('./importEndpoint');
const SpecEndpoint = require('./specEndpoint');

const app = module.exports = new Koa();
app.use(cors);
Expand All @@ -52,6 +53,7 @@ app.use(_.post('/web', WebEndpoint.handle.bind(WebEndpoint)));
app.use(_.post('/search', SearchEndpoint.handle.bind(SearchEndpoint)));
app.use(_.post('/export', ExportEndpoint.handle.bind(ExportEndpoint)));
app.use(_.post('/import', ImportEndpoint.handle.bind(ImportEndpoint)));
app.use(_.get('/', SpecEndpoint.handle.bind(SpecEndpoint)));

Debug.init(process.env.DEBUG_LEVEL ? parseInt(process.env.DEBUG_LEVEL) : 1);
Translators.init()
Expand Down
135 changes: 135 additions & 0 deletions src/specEndpoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
'use strict';

/*
***** BEGIN LICENSE BLOCK *****

Copyright © 2018 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://www.zotero.org

This file is part of Zotero.

Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.

***** END LICENSE BLOCK *****
*/

const path = require('path');
const fs = require('fs');
const docRoot = `${require('swagger-ui-dist').absolutePath()}/`;
const DOC_CSP = "default-src 'none'; " +
"script-src 'self' 'unsafe-inline'; connect-src *; " +
"style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';";
const yaml = require('js-yaml');

function processRequest(ctx) {

const reqPath = ctx.request.query.path || '/index.html';
const filePath = path.join(docRoot, reqPath);
let contentType;

// Disallow relative paths.
// Test relies on docRoot ending on a slash.
if (filePath.substring(0, docRoot.length) !== docRoot) {
Zotero.debug(`${reqPath} could not be found.`)
ctx.throw(404, "File not found\n");
}

let body = fs.readFileSync(filePath);
if (reqPath === '/index.html') {
const css = `
/* Removes Swagger's image from the header bar */
.topbar-wrapper .link img {
display: none;
}
/* Adds the application's name in the header bar */
.topbar-wrapper .link::after {
content: zotero;
}
/* Removes input field and explore button from header bar */
.swagger-ui .topbar .download-url-wrapper {
display: none;
}
/* Modifies the font in the information area */
.swagger-ui .info li, .swagger-ui .info p, .swagger-ui .info table, .swagger-ui .info a {
font-size: 16px;
line-height: 1.4em;
}
/* Removes authorize button and section */
.scheme-container {
display: none
}
`;
body = body.toString()
.replace(/((?:src|href)=['"])/g, '$1?doc&path=')
// Some self-promotion
.replace(/<\/style>/, `${css}\n </style>`)
.replace(/<title>[^<]*<\/title>/, `<title>Zotero's translation-server</title>`)
// Replace the default url with ours, switch off validation &
// limit the size of documents to apply syntax highlighting to
.replace(/dom_id: '#swagger-ui'/, 'dom_id: "#swagger-ui", ' +
'docExpansion: "none", defaultModelsExpandDepth: -1, validatorUrl: null, displayRequestDuration: true')
.replace(/"https:\/\/petstore.swagger.io\/v2\/swagger.json"/,
'"/?spec"');

contentType = 'text/html';
}
if (/\.js$/.test(reqPath)) {
contentType = 'text/javascript';
body = body.toString()
.replace(/underscore-min\.map/, '?doc&path=lib/underscore-min.map')
.replace(/sourceMappingURL=/, 'sourceMappingURL=/?doc&path=');
} else if (/\.png$/.test(reqPath)) {
contentType = 'image/png';
} else if (/\.map$/.test(reqPath)) {
contentType = 'application/json';
} else if (/\.ttf$/.test(reqPath)) {
contentType = 'application/x-font-ttf';
} else if (/\.css$/.test(reqPath)) {
contentType = 'text/css';
body = body.toString()
.replace(/\.\.\/(images|fonts)\//g, '?doc&path=$1/')
.replace(/sourceMappingURL=/, 'sourceMappingURL=/?doc&path=');
}

ctx.set('content-type', contentType);
ctx.set('content-security-policy', DOC_CSP);
ctx.set('x-content-security-policy', DOC_CSP);
ctx.set('x-webkit-csp', DOC_CSP);
ctx.response.body = (body.toString());
}

module.exports = {
handle: async function (ctx, _next) {

let spec = path.resolve('spec.yaml');

if (spec.constructor !== Object) {
try {
spec = yaml.load(fs.readFileSync(spec));;
} catch (e) {
spec = {};
}
}

if ({}.hasOwnProperty.call(ctx.request.query || {}, 'spec')) {
ctx.set('content-type', 'application/json');
ctx.response.body = spec;
} else if ({}.hasOwnProperty.call(ctx.request.query || {}, 'doc')) {
return processRequest(ctx);
} else {
_next();
}
}
}
18 changes: 18 additions & 0 deletions test/spec_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* global assert:false, request:false */

describe("/", function () {
it("should get doc page", async function () {
var response = await request()
.get('/?doc');
assert.equal(response.statusCode, 200);
});

it("should get spec json", async function () {
var response = await request()
.get('/?spec');
assert.equal(response.statusCode, 200);
var json = response.body;
assert.ok(json.openapi);
});

});