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

extend OpenAPI spec: define response body schema, bugfixes #15

Open
wants to merge 2 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
1 change: 1 addition & 0 deletions examples/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const config = {
description: 'An HTTP API for Deutsche Bahn.',
homepage: 'http://example.org/',
docsLink: 'http://example.org/docs',
openapiSpec: true,
logging: true,
healthCheck: async () => {
const stop = await hafas.stop('8011306')
Expand Down
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ const createHafasRestApi = async (hafas, config, attachMiddleware) => {
api.get(path, route)
}

if (config.openapiSpec) serveOpenapiSpec(api)
if (config.openapiSpec) serveOpenapiSpec(hafas, api)

const rootLinks = {}
for (const [path, route] of Object.entries(routes)) {
Expand Down
22 changes: 22 additions & 0 deletions lib/format-product-parameters.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,28 @@ const formatProductParams = (products) => {
return params
}

const formatProductsAsOpenapiParameters = (products) => {
const schema = formatProductParams(products);
for (let s in schema) {
delete schema[s].parse;
}
return {
type: 'object',
properties: schema
}
}

const profileSpecificProductsAsOpenapiParameters = () => {
return {
name: 'products',
in: 'query',
description: 'Filter by profile-specific products (e.g. regional transport only).',
schema: {'$ref': '#/components/schemas/ProfileSpecificProducts'}
}
}

export {
formatProductParams,
formatProductsAsOpenapiParameters,
profileSpecificProductsAsOpenapiParameters
}
19 changes: 19 additions & 0 deletions lib/generate-schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as TJS from "typescript-json-schema";

const generateOpenapiSchema = () => {
const files = ['node_modules/@types/hafas-client/index.d.ts'];
const program = TJS.getProgramFromFiles(files);
const schema = TJS.generateSchema(program, "*", {}, files);
let components = schema.definitions;
const openApiSchema = JSON.stringify(components)
.replaceAll('#/definitions/', '#/components/schemas/')
.replaceAll(/"type":\[.*?\]/g, '"type":"string"') // type as a list is not valid in OpenAPI, using string as default!
.replaceAll('"type":"null"', '"type":"string"') // type null is not valid in OpenAPI, using string as default!
.replaceAll(/"const":"([^"]+)"/g, '"enum":["$1"]'); // const is not valid in OpenAPI, using singular enum!

return JSON.parse(openApiSchema);
}

export {
generateOpenapiSchema
}
2 changes: 1 addition & 1 deletion lib/json-pretty-printing.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const configureJSONPrettyPrinting = (req, res) => {

const jsonPrettyPrintingOpenapiParam = {
name: 'pretty',
in: 'path',
in: 'query',
description: 'Pretty-print JSON responses?',
schema: {type: 'boolean'},
}
Expand Down
14 changes: 10 additions & 4 deletions lib/openapi-spec.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import LinkHeader from 'http-link-header'
import { generateOpenapiSchema } from './generate-schema.js'
import { formatProductsAsOpenapiParameters } from './format-product-parameters.js';

const openapiContentType = 'application/vnd.oai.openapi;version=3.0.3'

const generateSpec = (config, routes, logger = console) => {
const generateSpec = (hafas, config, routes, logger = console) => {
const schemaDefinitions = generateOpenapiSchema();
schemaDefinitions.ProfileSpecificProducts = formatProductsAsOpenapiParameters(hafas.profile.products);
const spec = {
openapi: '3.0.3',
info: {
Expand All @@ -14,6 +18,9 @@ const generateSpec = (config, routes, logger = console) => {
version: config.version,
},
paths: {},
components: {
schemas: schemaDefinitions
}
}
if (config.docsLink) {
spec.externalDocs = {
Expand Down Expand Up @@ -48,10 +55,9 @@ const setOpenapiLink = (res) => {
res.setHeader('Link', header.toString())
}

const serveOpenapiSpec = (api) => {
const serveOpenapiSpec = (hafas, api) => {
const {config, logger} = api.locals
const spec = generateSpec(config, api.routes, logger)

const spec = generateSpec(hafas, config, api.routes, logger)
api.get([
wellKnownPath,
'/openapi.json',
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"node": ">=18"
},
"dependencies": {
"@types/hafas-client": "^6.2.0",
"compression": "^1.7.2",
"cors": "^2.8.4",
"date-fns": "^2.12.0",
Expand All @@ -46,14 +47,15 @@
"pino": "^8.8.0",
"pino-http": "^8.2.1",
"shorthash": "0.0.2",
"stringify-entities": "^4.0.3"
"stringify-entities": "^4.0.3",
"typescript-json-schema": "^0.65.1"
},
"devDependencies": {
"axios": "^1.4.0",
"cached-hafas-client": "^5.0.0",
"eslint": "^8.0.1",
"get-port": "^7.0.0",
"hafas-client": "^6.0.0",
"hafas-client": "^6.3.3",
"ioredis": "^5.2.4",
"pino-pretty": "^10.0.1",
"tap-min": "^2.0.0",
Expand Down
5 changes: 3 additions & 2 deletions routes/arrivals.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
jsonPrettyPrintingParam,
} from '../lib/json-pretty-printing.js'
import {formatParsersAsOpenapiParams} from '../lib/format-parsers-as-openapi.js'
import {formatProductParams} from '../lib/format-product-parameters.js'
import {formatProductParams, profileSpecificProductsAsOpenapiParameters} from '../lib/format-product-parameters.js'

const err400 = (msg) => {
const err = new Error(msg)
Expand Down Expand Up @@ -138,6 +138,7 @@ Works like \`/stops/{id}/departures\`, except that it uses [\`hafasClient.arriva
// todo: examples?
},
...formatParsersAsOpenapiParams(parsers),
profileSpecificProductsAsOpenapiParameters(),
jsonPrettyPrintingOpenapiParam,
],
responses: {
Expand All @@ -150,7 +151,7 @@ Works like \`/stops/{id}/departures\`, except that it uses [\`hafasClient.arriva
properties: {
arrivals: {
type: 'array',
items: {type: 'object'}, // todo
items: {'$ref': '#/components/schemas/Alternative'},
},
realtimeDataUpdatedAt: {
type: 'integer',
Expand Down
5 changes: 3 additions & 2 deletions routes/departures.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
jsonPrettyPrintingParam,
} from '../lib/json-pretty-printing.js'
import {formatParsersAsOpenapiParams} from '../lib/format-parsers-as-openapi.js'
import {formatProductParams} from '../lib/format-product-parameters.js'
import {formatProductParams, profileSpecificProductsAsOpenapiParameters} from '../lib/format-product-parameters.js'

const MINUTE = 60 * 1000

Expand Down Expand Up @@ -150,6 +150,7 @@ Uses [\`hafasClient.departures()\`](https://github.com/public-transport/hafas-cl
// todo: examples?
},
...formatParsersAsOpenapiParams(parsers),
profileSpecificProductsAsOpenapiParameters(),
jsonPrettyPrintingOpenapiParam,
],
responses: {
Expand All @@ -162,7 +163,7 @@ Uses [\`hafasClient.departures()\`](https://github.com/public-transport/hafas-cl
properties: {
departures: {
type: 'array',
items: {type: 'object'}, // todo
items: {'$ref': '#/components/schemas/Alternative'},
},
realtimeDataUpdatedAt: {
type: 'integer',
Expand Down
61 changes: 59 additions & 2 deletions routes/journeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
jsonPrettyPrintingParam,
} from '../lib/json-pretty-printing.js'
import {formatParsersAsOpenapiParams} from '../lib/format-parsers-as-openapi.js'
import {formatProductParams} from '../lib/format-product-parameters.js'
import {formatProductParams, profileSpecificProductsAsOpenapiParameters} from '../lib/format-product-parameters.js'

const WITHOUT_FROM_TO = {
from: null,
Expand Down Expand Up @@ -214,7 +214,64 @@ Uses [\`hafasClient.journeys()\`](https://github.com/public-transport/hafas-clie
url: 'https://github.com/public-transport/hafas-client/blob/6/docs/journeys.md',
},
parameters: [
{
name: 'from',
in: 'query',
schema: {type: 'string'},
description: '"from" as stop/station ID (e.g. from=8010159 for Halle (Saale) Hbf)'
},
{
name: 'from.id',
in: 'query',
schema: {type: 'string'},
description: '"from" as POI (e.g. from.id=991561765&from.latitude=51.48364&from.longitude=11.98084 for Halle+(Saale),+Stadtpark+Halle+(Grünanlagen))'
},
{
name: 'from.address',
in: 'query',
schema: {type: 'string'},
description: '"from" as an address (e.g. from.latitude=51.25639&from.longitude=7.46685&from.address=Hansestadt+Breckerfeld,+Hansering+3 for Hansestadt Breckerfeld, Hansering 3)'
},
{
name: 'from.latitude',
in: 'query',
schema: {type: 'number'}
},
{
name: 'from.longitude',
in: 'query',
schema: {type: 'number'}
},
{
name: 'to',
in: 'query',
schema: {type: 'string'},
description: '"to" as stop/station ID'
},
{
name: 'to.id',
in: 'query',
schema: {type: 'string'},
description: '"to" as POI'
},
{
name: 'to.address',
in: 'query',
schema: {type: 'string'},
description: '"to" as an address'
},
{
name: 'to.latitude',
in: 'query',
schema: {type: 'number'}
},
{
name: 'to.longitude',
in: 'query',
schema: {type: 'number'}
},
...formatParsersAsOpenapiParams(parsers),
profileSpecificProductsAsOpenapiParameters(),
jsonPrettyPrintingOpenapiParam,
],
responses: {
Expand All @@ -227,7 +284,7 @@ Uses [\`hafasClient.journeys()\`](https://github.com/public-transport/hafas-clie
properties: {
journeys: {
type: 'array',
items: {type: 'object'}, // todo
items: {'$ref': '#/components/schemas/Journey'},
},
realtimeDataUpdatedAt: {
type: 'integer',
Expand Down
16 changes: 14 additions & 2 deletions routes/locations.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,20 @@ Uses [\`hafasClient.locations()\`](https://github.com/public-transport/hafas-cli
content: {
'application/json': {
schema: {
type: 'array',
items: {type: 'object'}, // todo
'type': 'array',
'items': {
'anyOf': [
{
'$ref': '#/components/schemas/Location'
},
{
'$ref': '#/components/schemas/Station'
},
{
'$ref': '#/components/schemas/Stop'
}
]
}
},
// todo: example(s)
},
Expand Down
21 changes: 19 additions & 2 deletions routes/nearby.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ Uses [\`hafasClient.nearby()\`](https://github.com/public-transport/hafas-client
url: 'https://github.com/public-transport/hafas-client/blob/6/docs/nearby.md',
},
parameters: [
{
name: 'location',
in: 'query',
schema: {'$ref': '#/components/schemas/Location'}
},
...formatParsersAsOpenapiParams(parsers),
jsonPrettyPrintingOpenapiParam,
],
Expand All @@ -101,8 +106,20 @@ Uses [\`hafasClient.nearby()\`](https://github.com/public-transport/hafas-client
content: {
'application/json': {
schema: {
type: 'array',
items: {type: 'object'}, // todo
'type': 'array',
'items': {
'anyOf': [
{
'$ref': '#/components/schemas/Location'
},
{
'$ref': '#/components/schemas/Station'
},
{
'$ref': '#/components/schemas/Stop'
}
]
}
},
// todo: example(s)
},
Expand Down
7 changes: 6 additions & 1 deletion routes/radar.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ Uses [\`hafasClient.radar()\`](https://github.com/public-transport/hafas-client/
url: 'https://github.com/public-transport/hafas-client/blob/6/docs/radar.md',
},
parameters: [
{
name: 'bbox',
in: 'query',
schema: {'$ref': '#/components/schemas/BoundingBox'}
},
...formatParsersAsOpenapiParams(parsers),
jsonPrettyPrintingOpenapiParam,
],
Expand All @@ -97,7 +102,7 @@ Uses [\`hafasClient.radar()\`](https://github.com/public-transport/hafas-client/
properties: {
movements: {
type: 'array',
items: {type: 'object'}, // todo
items: {'$ref': '#/components/schemas/Movement'},
},
realtimeDataUpdatedAt: {
type: 'integer',
Expand Down
10 changes: 8 additions & 2 deletions routes/reachable-from.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
jsonPrettyPrintingParam,
} from '../lib/json-pretty-printing.js'
import {formatParsersAsOpenapiParams} from '../lib/format-parsers-as-openapi.js'
import {formatProductParams} from '../lib/format-product-parameters.js'
import {formatProductParams, profileSpecificProductsAsOpenapiParameters} from '../lib/format-product-parameters.js'

const err400 = (msg) => {
const err = new Error(msg)
Expand Down Expand Up @@ -84,7 +84,13 @@ Uses [\`hafasClient.reachableFrom()\`](https://github.com/public-transport/hafas
url: 'https://github.com/public-transport/hafas-client/blob/6/docs/reachable-from.md',
},
parameters: [
{
name: 'address',
in: 'query',
schema: {'$ref': '#/components/schemas/Location'}
},
...formatParsersAsOpenapiParams(parsers),
profileSpecificProductsAsOpenapiParameters(),
jsonPrettyPrintingOpenapiParam,
],
responses: {
Expand All @@ -97,7 +103,7 @@ Uses [\`hafasClient.reachableFrom()\`](https://github.com/public-transport/hafas
properties: {
reachable: {
type: 'array',
items: {type: 'object'}, // todo
items: {'$ref': '#/components/schemas/Duration'},
},
realtimeDataUpdatedAt: {
type: 'integer',
Expand Down
3 changes: 1 addition & 2 deletions routes/refresh-journey.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,7 @@ The journey will be the same (equal \`from\`, \`to\`, \`via\`, date/time & vehic
type: 'object',
properties: {
journey: {
type: 'object',
// todo
'$ref': '#/components/schemas/Journey'
},
realtimeDataUpdatedAt: {
type: 'integer',
Expand Down
Loading
Loading