From 6484ab3b4d9946f6af875bad29f3cdd2d4ee9e89 Mon Sep 17 00:00:00 2001 From: Traines Date: Wed, 31 Aug 2022 17:30:58 +0000 Subject: [PATCH 1/2] openapi: add missing/fix request params --- lib/json-pretty-printing.js | 2 +- routes/journeys.js | 56 +++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/lib/json-pretty-printing.js b/lib/json-pretty-printing.js index f527178..753a339 100644 --- a/lib/json-pretty-printing.js +++ b/lib/json-pretty-printing.js @@ -7,7 +7,7 @@ const configureJSONPrettyPrinting = (req, res) => { const jsonPrettyPrintingOpenapiParam = { name: 'pretty', - in: 'path', + in: 'query', description: 'Pretty-print JSON responses?', schema: {type: 'boolean'}, } diff --git a/routes/journeys.js b/routes/journeys.js index eb1f429..b1d3617 100644 --- a/routes/journeys.js +++ b/routes/journeys.js @@ -214,6 +214,62 @@ 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), jsonPrettyPrintingOpenapiParam, ], From 70cf767993c7df193a56bc48edf19b9561bea09c Mon Sep 17 00:00:00 2001 From: Traines Date: Wed, 15 Jan 2025 01:30:19 +0000 Subject: [PATCH 2/2] openapi: generate response schema from @types/hafas-client --- examples/db.js | 1 + index.js | 2 +- lib/format-product-parameters.js | 22 ++++++++++++++++++++++ lib/generate-schema.js | 19 +++++++++++++++++++ lib/openapi-spec.js | 14 ++++++++++---- package.json | 6 ++++-- routes/arrivals.js | 5 +++-- routes/departures.js | 5 +++-- routes/journeys.js | 5 +++-- routes/locations.js | 16 ++++++++++++++-- routes/nearby.js | 21 +++++++++++++++++++-- routes/radar.js | 7 ++++++- routes/reachable-from.js | 10 ++++++++-- routes/refresh-journey.js | 3 +-- routes/stop.js | 13 +++++++++++-- routes/trip.js | 3 +-- routes/trips.js | 3 ++- 17 files changed, 128 insertions(+), 27 deletions(-) create mode 100644 lib/generate-schema.js diff --git a/examples/db.js b/examples/db.js index 27480ab..5f28289 100644 --- a/examples/db.js +++ b/examples/db.js @@ -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') diff --git a/index.js b/index.js index 0561359..f6a0a69 100644 --- a/index.js +++ b/index.js @@ -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)) { diff --git a/lib/format-product-parameters.js b/lib/format-product-parameters.js index 7ff11c0..6d999cc 100644 --- a/lib/format-product-parameters.js +++ b/lib/format-product-parameters.js @@ -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 } diff --git a/lib/generate-schema.js b/lib/generate-schema.js new file mode 100644 index 0000000..9887929 --- /dev/null +++ b/lib/generate-schema.js @@ -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 +} \ No newline at end of file diff --git a/lib/openapi-spec.js b/lib/openapi-spec.js index 37be14b..e08107a 100644 --- a/lib/openapi-spec.js +++ b/lib/openapi-spec.js @@ -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: { @@ -14,6 +18,9 @@ const generateSpec = (config, routes, logger = console) => { version: config.version, }, paths: {}, + components: { + schemas: schemaDefinitions + } } if (config.docsLink) { spec.externalDocs = { @@ -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', diff --git a/package.json b/package.json index 5fa5b16..9e6fe02 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/routes/arrivals.js b/routes/arrivals.js index ba44589..78c04d0 100644 --- a/routes/arrivals.js +++ b/routes/arrivals.js @@ -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) @@ -138,6 +138,7 @@ Works like \`/stops/{id}/departures\`, except that it uses [\`hafasClient.arriva // todo: examples? }, ...formatParsersAsOpenapiParams(parsers), + profileSpecificProductsAsOpenapiParameters(), jsonPrettyPrintingOpenapiParam, ], responses: { @@ -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', diff --git a/routes/departures.js b/routes/departures.js index e70f3a1..8f3da3a 100644 --- a/routes/departures.js +++ b/routes/departures.js @@ -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 @@ -150,6 +150,7 @@ Uses [\`hafasClient.departures()\`](https://github.com/public-transport/hafas-cl // todo: examples? }, ...formatParsersAsOpenapiParams(parsers), + profileSpecificProductsAsOpenapiParameters(), jsonPrettyPrintingOpenapiParam, ], responses: { @@ -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', diff --git a/routes/journeys.js b/routes/journeys.js index b1d3617..5d62144 100644 --- a/routes/journeys.js +++ b/routes/journeys.js @@ -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, @@ -271,6 +271,7 @@ Uses [\`hafasClient.journeys()\`](https://github.com/public-transport/hafas-clie schema: {type: 'number'} }, ...formatParsersAsOpenapiParams(parsers), + profileSpecificProductsAsOpenapiParameters(), jsonPrettyPrintingOpenapiParam, ], responses: { @@ -283,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', diff --git a/routes/locations.js b/routes/locations.js index b77916b..0311ce6 100644 --- a/routes/locations.js +++ b/routes/locations.js @@ -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) }, diff --git a/routes/nearby.js b/routes/nearby.js index fb73fc3..903935a 100644 --- a/routes/nearby.js +++ b/routes/nearby.js @@ -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, ], @@ -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) }, diff --git a/routes/radar.js b/routes/radar.js index 18970cd..a635d85 100644 --- a/routes/radar.js +++ b/routes/radar.js @@ -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, ], @@ -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', diff --git a/routes/reachable-from.js b/routes/reachable-from.js index a199ca0..704dca9 100644 --- a/routes/reachable-from.js +++ b/routes/reachable-from.js @@ -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) @@ -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: { @@ -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', diff --git a/routes/refresh-journey.js b/routes/refresh-journey.js index 1261f6a..ba1aea4 100644 --- a/routes/refresh-journey.js +++ b/routes/refresh-journey.js @@ -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', diff --git a/routes/stop.js b/routes/stop.js index abfba99..0a2b154 100644 --- a/routes/stop.js +++ b/routes/stop.js @@ -75,8 +75,17 @@ Uses [\`hafasClient.stop()\`](https://github.com/public-transport/hafas-client/b content: { 'application/json': { schema: { - type: 'object', - // todo + 'anyOf': [ + { + '$ref': '#/components/schemas/Location' + }, + { + '$ref': '#/components/schemas/Station' + }, + { + '$ref': '#/components/schemas/Stop' + } + ] }, // todo: example(s) }, diff --git a/routes/trip.js b/routes/trip.js index 22cc877..afda2cb 100644 --- a/routes/trip.js +++ b/routes/trip.js @@ -93,8 +93,7 @@ Uses [\`hafasClient.trip()\`](https://github.com/public-transport/hafas-client/b type: 'object', properties: { trip: { - type: 'object', - // todo + '$ref': '#/components/schemas/Trip' }, realtimeDataUpdatedAt: { type: 'integer', diff --git a/routes/trips.js b/routes/trips.js index f5d1ddb..554f7ce 100644 --- a/routes/trips.js +++ b/routes/trips.js @@ -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 MINUTE = 60 * 1000 @@ -139,6 +139,7 @@ Uses [\`hafasClient.tripsByName()\`](https://github.com/public-transport/hafas-c }, parameters: [ ...formatParsersAsOpenapiParams(parsers), + profileSpecificProductsAsOpenapiParameters(), jsonPrettyPrintingOpenapiParam, ], responses: {