-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
TypeScript OpenAPI code generation (#24)
* add openapi typescript definitions (partly 3rd party) * add simple typescript generation script * fix deprecation warning * Update README.md * remove commented code blocks
- Loading branch information
Showing
6 changed files
with
568 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
// Generated by ./scripts/gen_openapi_types.ts | ||
|
||
/** PATHS **/ | ||
|
||
// /api/polling_stations/{id}/data_entries/{entry_number} | ||
export interface POLLING_STATION_DATA_ENTRY_REQUEST_PARAMS { | ||
id: number; | ||
entry_number: number; | ||
} | ||
export type POLLING_STATION_DATA_ENTRY_REQUEST_PATH = | ||
`/api/polling_stations/${number}/data_entries/${number};`; | ||
export type POLLING_STATION_DATA_ENTRY_REQUEST_BODY = DataEntryRequest; | ||
|
||
/** TYPES **/ | ||
|
||
export interface DataEntryError { | ||
message: string; | ||
} | ||
|
||
/** | ||
* Payload structure for data entry of polling station results | ||
*/ | ||
export interface DataEntryRequest { | ||
data: PollingStationResults; | ||
} | ||
|
||
/** | ||
* PollingStationResults, following the fields in | ||
"Model N 10-1. Proces-verbaal van een stembureau" | ||
<https://wetten.overheid.nl/BWBR0034180/2023-11-01#Bijlage1_DivisieN10.1> | ||
*/ | ||
export interface PollingStationResults { | ||
voters_counts: VotersCounts; | ||
votes_counts: VotesCounts; | ||
} | ||
|
||
/** | ||
* Voters counts, part of the polling station results. | ||
*/ | ||
export interface VotersCounts { | ||
poll_card_count: number; | ||
proxy_certificate_count: number; | ||
total_admitted_voters_count: number; | ||
voter_card_count: number; | ||
} | ||
|
||
/** | ||
* Votes counts, part of the polling station results. | ||
*/ | ||
export interface VotesCounts { | ||
blank_votes_count: number; | ||
invalid_votes_count: number; | ||
total_votes_cast_count: number; | ||
votes_candidates_counts: number; | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import assert from "assert"; | ||
import fs from "fs"; | ||
import prettier from "prettier"; | ||
import { OpenAPIV3, ReferenceObject, SchemaObject, PathsObject, OperationObject } from "./openapi"; | ||
|
||
const TARGET_PATH = "./lib/api/gen"; | ||
const FILE_NAME = "openapi.ts"; | ||
|
||
async function run() { | ||
const fileString = fs.readFileSync("../backend/openapi.json", "utf8"); | ||
if (!fileString) { | ||
return; | ||
} | ||
|
||
const spec = JSON.parse(fileString) as OpenAPIV3; | ||
|
||
if (fs.existsSync(TARGET_PATH)) { | ||
fs.rmSync(TARGET_PATH, { recursive: true }); | ||
} | ||
fs.mkdirSync(TARGET_PATH); | ||
|
||
const result = ["// Generated by ./scripts/gen_openapi_types.ts\n\n"]; | ||
assert(spec["components"] !== undefined); | ||
|
||
const schemas = spec.components.schemas; | ||
if (!schemas) { | ||
throw new Error("No schemas found in OpenAPI spec"); | ||
} | ||
|
||
const paths: string[] = Object.entries(spec.paths).map(([k, v]) => addPath(k, v)); | ||
const types: string[] = Object.entries(schemas).map(([k, v]) => addDefinition(k, v)); | ||
|
||
result.push("\n\n"); | ||
|
||
result.push("/** PATHS **/\n\n"); | ||
result.push(paths.join("\n\n")); | ||
|
||
result.push("\n\n"); | ||
|
||
result.push("/** TYPES **/\n\n"); | ||
result.push(types.join("\n\n")); | ||
|
||
let s = result.join("\n"); | ||
s = await prettier.format(s, { parser: "typescript" }); | ||
fs.writeFileSync(`${TARGET_PATH}/${FILE_NAME}`, s); | ||
} | ||
|
||
run().catch((e: unknown) => { | ||
console.error(e); | ||
}); | ||
|
||
function addPath(path: string, v: PathsObject | undefined) { | ||
if (!v) return ""; | ||
const result: string[] = [`// ${path}`]; | ||
let requestPath = path; | ||
if (v.post) { | ||
const post = v.post as OperationObject; | ||
|
||
assert(typeof post.operationId === "string"); | ||
let id: string = post.operationId; | ||
id = id.toUpperCase(); | ||
|
||
result.push(`export interface ${id}_REQUEST_PARAMS {`); | ||
if (post.parameters) { | ||
post.parameters.forEach((p) => { | ||
if ("$ref" in p) { | ||
result.push(`// ${p.$ref.substring(p.$ref.lastIndexOf("/") + 1)}`); | ||
} else { | ||
const paramType = tsType(p.schema); | ||
requestPath = requestPath.replace(`{${p.name}}`, `\${${paramType}}`); | ||
result.push(`${p.name}: ${paramType};`); | ||
} | ||
}); | ||
} | ||
result.push("}"); | ||
result.push(`export type ${id}_REQUEST_PATH = \`${requestPath};\``); | ||
|
||
if (post.requestBody) { | ||
if ("$ref" in post.requestBody) { | ||
result.push( | ||
`export type ${id}_REQUEST_BODY = ${post.requestBody.$ref.substring(post.requestBody.$ref.lastIndexOf("/") + 1)};` | ||
); | ||
} else { | ||
const media = post.requestBody.content["application/json"]; | ||
if (media?.schema) { | ||
if ("$ref" in media.schema) { | ||
result.push( | ||
`export type ${id}_REQUEST_BODY = ${media.schema.$ref.substring(media.schema.$ref.lastIndexOf("/") + 1)};` | ||
); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
return result.join("\n"); | ||
} | ||
|
||
function addDefinition(name: string, v: ReferenceObject | SchemaObject) { | ||
if ("$ref" in v) { | ||
return v.$ref.substring(v.$ref.lastIndexOf("/") + 1); | ||
} | ||
|
||
const result: string[] = []; | ||
if (v.description) { | ||
result.push("/**"); | ||
result.push(` * ${v.description}`); | ||
result.push(" */"); | ||
} | ||
result.push(`export interface ${name} {`); | ||
if (v.type === "object") { | ||
if (v.properties) { | ||
Object.entries(v.properties).forEach(([k, v2]) => { | ||
result.push(` ${k}${isRequired(k, v.required)}: ${tsType(v2)};`); | ||
}); | ||
} | ||
} | ||
result.push("}"); | ||
|
||
return result.join("\n"); | ||
} | ||
|
||
function tsType(s: ReferenceObject | SchemaObject | undefined): string { | ||
//TODO: handle missing schema | ||
if (!s) return "string"; | ||
if ("$ref" in s) { | ||
return s.$ref.substring(s.$ref.lastIndexOf("/") + 1); | ||
} | ||
|
||
switch (s.type) { | ||
case "string": | ||
return "string"; | ||
case "integer": | ||
return "number"; | ||
case "number": | ||
return "number"; | ||
default: | ||
//TODO: catch all types, any is not allowed | ||
return "any"; | ||
} | ||
} | ||
|
||
function isRequired(k: string, req: string[] | undefined): string { | ||
if (!req) { | ||
return ""; | ||
} | ||
return req.includes(k) ? "" : "?"; | ||
} |
Oops, something went wrong.