Skip to content

Commit 918b6c8

Browse files
committed
Add OpenApi spec and Swagger documentation
Add Swagger documentation to the root http://127.0.0.1:1969/?doc with OpenApi 3.0 specification at http://127.0.0.1:1969/?spec Addresses zotero#76 Change-Id: Ide7b45e7dca90b3ccbbf8141358a66f8dc7b1187
1 parent 760d6ca commit 918b6c8

File tree

6 files changed

+304
-0
lines changed

6 files changed

+304
-0
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ It’s also possible to opt out of proxying for specific hosts by using the `NO_
7979

8080
## Endpoints
8181

82+
### Documentation
83+
84+
Swagger documentation is available at http://127.0.0.1:1969/?doc
85+
8286
### Web Translation
8387

8488
#### Retrieve metadata for a webpage:

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"aws-sdk": "^2.326.0",
1313
"config": "^1.30.0",
1414
"iconv-lite": "^0.4.24",
15+
"js-yaml": "^4.1.0",
1516
"jsdom": "^13.1.0",
1617
"koa": "^2.13.1",
1718
"koa-bodyparser": "^4.3.0",
@@ -20,6 +21,7 @@
2021
"request": "^2.87.0",
2122
"request-promise-native": "^1.0.5",
2223
"serverless-http": "^1.6.0",
24+
"swagger-ui-dist": "^3.52.3",
2325
"wicked-good-xpath": "git+https://git@github.com/zotero/wicked-good-xpath.git#1b88459",
2426
"xregexp": "^4.2.0",
2527
"yargs": "^12.0.2"

spec.yaml

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
openapi: 3.0.0
2+
info:
3+
version: 1.0.0
4+
title: Zotero translation-server
5+
description: The Zotero translation server lets you use Zotero translators without the Zotero client.
6+
license:
7+
name: AGPL-3.0-only
8+
url: https://spdx.org/licenses/AGPL-3.0-only.html
9+
paths:
10+
/web:
11+
post:
12+
tags:
13+
- Web
14+
description: Retrieve metadata for a webpage
15+
requestBody:
16+
required: true
17+
content:
18+
text/plain:
19+
schema:
20+
type: string
21+
example: http://www.example.com
22+
produces:
23+
- application/json; charset=utf-8;
24+
responses:
25+
'200':
26+
description: Returns an array of translated items in Zotero API JSON format
27+
schema:
28+
type: Array
29+
'300':
30+
description: Multiple items found
31+
schema:
32+
type: Object
33+
properties:
34+
url:
35+
type: string
36+
session:
37+
type: string
38+
items:
39+
type: Object
40+
'400':
41+
description: Remote page not found
42+
schema:
43+
type: string
44+
/search:
45+
post:
46+
tags:
47+
- Search
48+
description: Retrieve metadata from an identifier (DOI, ISBN, PMID, arXiv ID)
49+
requestBody:
50+
required: true
51+
content:
52+
text/plain:
53+
schema:
54+
type: string
55+
encoding:
56+
allowReserved: true
57+
produces:
58+
- application/json; charset=utf-8;
59+
responses:
60+
'200':
61+
description: The citation data in the requested format
62+
schema:
63+
type: Array
64+
'501':
65+
description: Metadata for identifier was not found
66+
schema:
67+
type: string
68+
/export:
69+
post:
70+
tags:
71+
- Export
72+
description: Convert items in Zotero API JSON format to a supported export format (RIS, BibTeX, etc.)
73+
requestBody:
74+
required: true
75+
content:
76+
application/json:
77+
schema:
78+
type: array
79+
parameters:
80+
- name: format
81+
in: query
82+
description: Requested format to covert body to
83+
schema:
84+
type: string
85+
enum:
86+
- bibtex
87+
- biblatex
88+
- bookmarks
89+
- coins
90+
- csljson
91+
- csv
92+
- endnote_xml
93+
- evernote
94+
- mods
95+
- rdf_bibliontology
96+
- rdf_dc
97+
- rdf_zotero
98+
- refer
99+
- refworks_tagged
100+
- ris
101+
- tei
102+
- wikipedia
103+
required: true
104+
produces:
105+
- application/json; charset=utf-8;
106+
responses:
107+
'200':
108+
description: The citation data in the requested format
109+
'415':
110+
description: Unsupported media type
111+
schema:
112+
type: string
113+
'501':
114+
description: Failed to translate
115+
schema:
116+
type: string
117+
/import:
118+
post:
119+
tags:
120+
- Import
121+
description: Convert items in any import format to the Zotero API JSON format
122+
requestBody:
123+
required: true
124+
content:
125+
text/plain:
126+
schema:
127+
type: string
128+
produces:
129+
- application/json; charset=utf-8;
130+
responses:
131+
'200':
132+
description: The citation data in the requested format
133+
schema:
134+
type: array
135+
'415':
136+
description: Unsupported media type
137+
schema:
138+
type: string
139+
'500':
140+
description: No suitable translators found
141+
schema:
142+
type: string

src/server.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ require('./zotero');
4141
const Debug = require('./debug');
4242
const Translators = require('./translators');
4343
const SearchEndpoint = require('./searchEndpoint');
44+
const SpecEndpoint = require('./specEndpoint');
4445
const WebEndpoint = require('./webEndpoint');
4546
const ExportEndpoint = require('./exportEndpoint');
4647
const ImportEndpoint = require('./importEndpoint');
@@ -52,6 +53,7 @@ app.use(_.post('/web', WebEndpoint.handle.bind(WebEndpoint)));
5253
app.use(_.post('/search', SearchEndpoint.handle.bind(SearchEndpoint)));
5354
app.use(_.post('/export', ExportEndpoint.handle.bind(ExportEndpoint)));
5455
app.use(_.post('/import', ImportEndpoint.handle.bind(ImportEndpoint)));
56+
app.use(_.get('/', SpecEndpoint.handle.bind(SpecEndpoint)));
5557

5658
Debug.init(process.env.DEBUG_LEVEL ? parseInt(process.env.DEBUG_LEVEL) : 1);
5759
Translators.init()

src/specEndpoint.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
'use strict';
2+
3+
/*
4+
***** BEGIN LICENSE BLOCK *****
5+
6+
Copyright © 2018 Corporation for Digital Scholarship
7+
Vienna, Virginia, USA
8+
https://www.zotero.org
9+
10+
This file is part of Zotero.
11+
12+
Zotero is free software: you can redistribute it and/or modify
13+
it under the terms of the GNU Affero General Public License as published by
14+
the Free Software Foundation, either version 3 of the License, or
15+
(at your option) any later version.
16+
17+
Zotero is distributed in the hope that it will be useful,
18+
but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
GNU Affero General Public License for more details.
21+
22+
You should have received a copy of the GNU Affero General Public License
23+
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
24+
25+
***** END LICENSE BLOCK *****
26+
*/
27+
28+
const path = require('path');
29+
const fs = require('fs');
30+
const docRoot = `${require('swagger-ui-dist').absolutePath()}/`;
31+
const DOC_CSP = "default-src 'none'; " +
32+
"script-src 'self' 'unsafe-inline'; connect-src *; " +
33+
"style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';";
34+
const yaml = require('js-yaml');
35+
36+
function processRequest(ctx) {
37+
38+
const reqPath = ctx.request.query.path || '/index.html';
39+
const filePath = path.join(docRoot, reqPath);
40+
let contentType;
41+
42+
// Disallow relative paths.
43+
// Test relies on docRoot ending on a slash.
44+
if (filePath.substring(0, docRoot.length) !== docRoot) {
45+
Zotero.debug(`${reqPath} could not be found.`)
46+
ctx.throw(404, "File not found\n");
47+
}
48+
49+
let body = fs.readFileSync(filePath);
50+
if (reqPath === '/index.html') {
51+
const css = `
52+
/* Removes Swagger's image from the header bar */
53+
.topbar-wrapper .link img {
54+
display: none;
55+
}
56+
/* Adds the application's name in the header bar */
57+
.topbar-wrapper .link::after {
58+
content: zotero;
59+
}
60+
/* Removes input field and explore button from header bar */
61+
.swagger-ui .topbar .download-url-wrapper {
62+
display: none;
63+
}
64+
/* Modifies the font in the information area */
65+
.swagger-ui .info li, .swagger-ui .info p, .swagger-ui .info table, .swagger-ui .info a {
66+
font-size: 16px;
67+
line-height: 1.4em;
68+
}
69+
/* Removes authorize button and section */
70+
.scheme-container {
71+
display: none
72+
}
73+
`;
74+
body = body.toString()
75+
.replace(/((?:src|href)=['"])/g, '$1?doc&path=')
76+
// Some self-promotion
77+
.replace(/<\/style>/, `${css}\n </style>`)
78+
.replace(/<title>[^<]*<\/title>/, `<title>Zotero's translation-server</title>`)
79+
// Replace the default url with ours, switch off validation &
80+
// limit the size of documents to apply syntax highlighting to
81+
.replace(/dom_id: '#swagger-ui'/, 'dom_id: "#swagger-ui", ' +
82+
'docExpansion: "none", defaultModelsExpandDepth: -1, validatorUrl: null, displayRequestDuration: true')
83+
.replace(/"https:\/\/petstore.swagger.io\/v2\/swagger.json"/,
84+
'"/?spec"');
85+
86+
contentType = 'text/html';
87+
}
88+
if (/\.js$/.test(reqPath)) {
89+
contentType = 'text/javascript';
90+
body = body.toString()
91+
.replace(/underscore-min\.map/, '?doc&path=lib/underscore-min.map')
92+
.replace(/sourceMappingURL=/, 'sourceMappingURL=/?doc&path=');
93+
} else if (/\.png$/.test(reqPath)) {
94+
contentType = 'image/png';
95+
} else if (/\.map$/.test(reqPath)) {
96+
contentType = 'application/json';
97+
} else if (/\.ttf$/.test(reqPath)) {
98+
contentType = 'application/x-font-ttf';
99+
} else if (/\.css$/.test(reqPath)) {
100+
contentType = 'text/css';
101+
body = body.toString()
102+
.replace(/\.\.\/(images|fonts)\//g, '?doc&path=$1/')
103+
.replace(/sourceMappingURL=/, 'sourceMappingURL=/?doc&path=');
104+
}
105+
106+
ctx.set('content-type', contentType);
107+
ctx.set('content-security-policy', DOC_CSP);
108+
ctx.set('x-content-security-policy', DOC_CSP);
109+
ctx.set('x-webkit-csp', DOC_CSP);
110+
ctx.response.body = (body.toString());
111+
112+
}
113+
114+
module.exports = {
115+
handle: async function (ctx, _next) {
116+
117+
let spec = path.resolve('spec.yaml');
118+
119+
if (spec.constructor !== Object) {
120+
try {
121+
spec = yaml.load(fs.readFileSync(spec));;
122+
} catch (e) {
123+
spec = {};
124+
}
125+
}
126+
127+
if ({}.hasOwnProperty.call(ctx.request.query || {}, 'spec')) {
128+
ctx.set('content-type', 'application/json');
129+
ctx.response.body = spec;
130+
} else if ({}.hasOwnProperty.call(ctx.request.query || {}, 'doc')) {
131+
return processRequest(ctx);
132+
} else {
133+
_next();
134+
}
135+
}
136+
}

test/spec_test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/* global assert:false, request:false */
2+
3+
describe("/", function () {
4+
it("should get doc page", async function () {
5+
var response = await request()
6+
.get('/?doc');
7+
assert.equal(response.statusCode, 200);
8+
});
9+
10+
it("should get spec json", async function () {
11+
var response = await request()
12+
.get('/?spec');
13+
assert.equal(response.statusCode, 200);
14+
var json = response.body;
15+
assert.ok(json.openapi);
16+
});
17+
18+
});

0 commit comments

Comments
 (0)