Skip to content

Commit 91b40c6

Browse files
authored
Toward portability (#235)
* delete .vscode * clean up gen.sh a bit * move io inits inside functions, separate initIO from initTestIO * move all node:process usage out to (almost) top level * take file copy out of io * clean up Api.ts file header * simplify IO situation * move static file copy out of generateApi * make it so you can run the script directly with tsx from anywhere * clean up generator/index.ts
1 parent ec8bbd3 commit 91b40c6

File tree

12 files changed

+482
-1043
lines changed

12 files changed

+482
-1043
lines changed

.vscode/launch.json

Lines changed: 0 additions & 18 deletions
This file was deleted.

client/Api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import type { FetchParams } from "./http-client";
1212
import { HttpClient, toQueryString } from "./http-client";
13+
1314
export type {
1415
ApiConfig,
1516
ApiResult,

generator/client/api.ts

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
* Copyright Oxide Computer Company
77
*/
88

9+
import fs from "node:fs";
10+
import assert from "node:assert";
11+
import { fileURLToPath } from "node:url";
12+
import path from "node:path";
13+
914
import type { OpenAPIV3 } from "openapi-types";
1015
import { OpenAPIV3 as O } from "openapi-types";
1116
const HttpMethods = O.HttpMethods;
12-
import assert from "assert";
1317
import {
1418
extractDoc,
1519
pathToTemplateStr,
@@ -29,9 +33,6 @@ import {
2933
} from "./base";
3034
import { schemaToTypes } from "../schema/types";
3135

32-
const io = initIO("Api.ts");
33-
const { w, w0, out, copy } = io;
34-
3536
/**
3637
* `Error` is hard-coded into `http-client.ts` as `ErrorBody` so we can check
3738
* and test that file statically, i.e., without doing any generation. We just
@@ -62,33 +63,44 @@ function checkErrorSchema(schema: Schema) {
6263
const queryParamsType = (opId: string) => `${opId}QueryParams`;
6364
const pathParamsType = (opId: string) => `${opId}PathParams`;
6465

65-
export function generateApi(spec: OpenAPIV3.Document) {
66+
/**
67+
* Source file is a relative path that we resolve relative to this
68+
* file, not the CWD or package root
69+
*/
70+
function copyFile(sourceRelPath: string, destDirAbs: string) {
71+
const thisFileDir = path.dirname(fileURLToPath(import.meta.url));
72+
const sourceAbsPath = path.resolve(thisFileDir, sourceRelPath);
73+
const destAbs = path.resolve(destDirAbs, path.basename(sourceRelPath));
74+
fs.copyFileSync(sourceAbsPath, destAbs);
75+
}
76+
77+
export function copyStaticFiles(destDir: string) {
78+
copyFile("../../static/util.ts", destDir);
79+
copyFile("../../static/http-client.ts", destDir);
80+
}
81+
82+
export function generateApi(spec: OpenAPIV3.Document, destDir: string) {
6683
if (!spec.components) return;
6784

68-
w("/* eslint-disable */\n");
85+
const outFile = path.resolve(destDir, "Api.ts");
86+
const out = fs.createWriteStream(outFile, { flags: "w" });
87+
const io = initIO(out);
88+
const { w, w0 } = io;
89+
90+
w(`/* eslint-disable */
6991
70-
w(`
7192
/**
7293
* This Source Code Form is subject to the terms of the Mozilla Public
7394
* License, v. 2.0. If a copy of the MPL was not distributed with this
7495
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
7596
*
7697
* Copyright Oxide Computer Company
7798
*/
78-
`);
79-
80-
copy("./static/util.ts");
81-
copy("./static/http-client.ts");
8299
83-
w(`import type { FetchParams } from './http-client'
84-
import { HttpClient, toQueryString } from './http-client'`);
100+
import type { FetchParams } from './http-client'
101+
import { HttpClient, toQueryString } from './http-client'
85102
86-
w(`export type {
87-
ApiConfig,
88-
ApiResult,
89-
ErrorBody,
90-
ErrorResult,
91-
} from './http-client'
103+
export type { ApiConfig, ApiResult, ErrorBody, ErrorResult, } from './http-client'
92104
`);
93105

94106
const schemaNames = getSortedSchemas(spec);

generator/client/msw-handlers.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,20 @@ import { initIO } from "../io";
1111
import { refToSchemaName } from "../schema/base";
1212
import { snakeToCamel, snakeToPascal } from "../util";
1313
import { contentRef, iterPathConfig } from "./base";
14-
15-
const io = initIO("msw-handlers.ts");
16-
const { w } = io;
14+
import path from "path";
15+
import fs from "fs";
1716

1817
const formatPath = (path: string) =>
1918
path.replace(/{(\w+)}/g, (n) => `:${snakeToCamel(n.slice(1, -1))}`);
2019

21-
export function generateMSWHandlers(spec: OpenAPIV3.Document) {
20+
export function generateMSWHandlers(spec: OpenAPIV3.Document, destDir: string) {
2221
if (!spec.components) return;
2322

23+
const outFile = path.resolve(destDir, "msw-handlers.ts");
24+
const out = fs.createWriteStream(outFile, { flags: "w" });
25+
const io = initIO(out);
26+
const { w } = io;
27+
2428
w(`
2529
/**
2630
* This Source Code Form is subject to the terms of the Mozilla Public

generator/client/type-tests.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@
99
import type { OpenAPIV3 } from "openapi-types";
1010
import { initIO } from "../io";
1111
import { getSortedSchemas } from "./base";
12+
import fs from "fs";
13+
import path from "path";
1214

13-
const io = initIO("type-test.ts");
14-
const { w } = io;
15-
16-
export function generateTypeTests(spec: OpenAPIV3.Document) {
15+
export function generateTypeTests(spec: OpenAPIV3.Document, destDir: string) {
1716
if (!spec.components) return;
1817

18+
const outFile = path.resolve(destDir, "type-test.ts");
19+
const out = fs.createWriteStream(outFile, { flags: "w" });
20+
const io = initIO(out);
21+
const { w } = io;
22+
1923
const schemaNames = getSortedSchemas(spec).filter((name) => name !== "Error");
2024

2125
w(`

generator/client/zodValidators.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,22 @@ import { initIO } from "../io";
1111
import { schemaToZod } from "../schema/zod";
1212
import { extractDoc, processParamName, snakeToPascal } from "../util";
1313
import { docComment, getSortedSchemas } from "./base";
14+
import path from "path";
15+
import fs from "fs";
1416

1517
const HttpMethods = OpenAPIV3.HttpMethods;
1618

17-
const io = initIO("validate.ts");
18-
const { w, w0, out } = io;
19-
20-
export function generateZodValidators(spec: OpenAPIV3.Document) {
19+
export function generateZodValidators(
20+
spec: OpenAPIV3.Document,
21+
destDir: string
22+
) {
2123
if (!spec.components) return;
2224

25+
const outFile = path.resolve(destDir, "validate.ts");
26+
const out = fs.createWriteStream(outFile, { flags: "w" });
27+
const io = initIO(out);
28+
const { w, w0 } = io;
29+
2330
w(`/* eslint-disable */
2431
2532
/**

generator/index.ts

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,42 @@
99
import SwaggerParser from "@apidevtools/swagger-parser";
1010
import type { OpenAPIV3 } from "openapi-types";
1111

12-
import { generateApi } from "./client/api";
12+
import { copyStaticFiles, generateApi } from "./client/api";
1313
import { generateMSWHandlers } from "./client/msw-handlers";
1414
import { generateTypeTests } from "./client/type-tests";
1515
import { generateZodValidators } from "./client/zodValidators";
16+
import { resolve } from "path";
1617

17-
const specFile = process.argv[2];
18-
if (!specFile) {
19-
throw Error("Missing specFile argument");
18+
async function generate(specFile: string, destDir: string) {
19+
// destination directory is resolved relative to CWD
20+
const destDirAbs = resolve(process.cwd(), destDir);
21+
22+
const rawSpec = await SwaggerParser.parse(specFile);
23+
if (!("openapi" in rawSpec) || !rawSpec.openapi.startsWith("3.0")) {
24+
throw new Error("Only OpenAPI 3.0 is currently supported");
25+
}
26+
27+
// we're not actually changing anything from rawSpec to spec, we've
28+
// just ruled out v2 and v3.1
29+
const spec = rawSpec as OpenAPIV3.Document;
30+
31+
copyStaticFiles(destDirAbs);
32+
generateApi(spec, destDirAbs);
33+
generateZodValidators(spec, destDirAbs);
34+
// TODO: make conditional - we only want generated for testing purpose
35+
generateTypeTests(spec, destDirAbs);
36+
generateMSWHandlers(spec, destDirAbs);
2037
}
2138

22-
SwaggerParser.parse(specFile).then((spec) => {
23-
generateApi(spec as OpenAPIV3.Document);
24-
generateZodValidators(spec as OpenAPIV3.Document);
25-
generateTypeTests(spec as OpenAPIV3.Document);
26-
generateMSWHandlers(spec as OpenAPIV3.Document);
27-
});
39+
function helpAndExit(msg: string): never {
40+
console.log(msg);
41+
console.log("\nUsage: gen <specFile> <destDir>");
42+
process.exit(1);
43+
}
44+
45+
const [specFile, destDir] = process.argv.slice(2);
46+
47+
if (!specFile) helpAndExit(`Missing <specFile>`);
48+
if (!destDir) helpAndExit(`Missing <destdir>`);
49+
50+
generate(specFile, destDir);

generator/io.ts

Lines changed: 15 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,23 @@
66
* Copyright Oxide Computer Company
77
*/
88

9-
import path from "path";
10-
import fs from "fs";
119
import { Writable } from "stream";
1210

11+
export interface IO {
12+
w: (str: string) => void;
13+
w0: (str: string) => void;
14+
}
15+
16+
// not a class because we want to destructure w and w0 in the calling code, and
17+
// if it was a class, they would lose their 'this' on destructure
18+
export function initIO(out: Writable) {
19+
return {
20+
w: (s: string) => out.write(s + "\n"),
21+
/** same as w() but no newline */
22+
w0: (s: string) => out.write(s),
23+
};
24+
}
25+
1326
/**
1427
* A test stream that stores a buffer internally
1528
*/
@@ -33,46 +46,3 @@ export class TestWritable extends Writable {
3346
this.buffer = Buffer.from("");
3447
}
3548
}
36-
37-
export interface IO<O extends Writable = Writable> {
38-
w: (str: string) => void;
39-
w0: (str: string) => void;
40-
copy: (file: string) => void;
41-
out: O;
42-
}
43-
44-
export function initIO(): IO<TestWritable>;
45-
export function initIO(outFile: string): IO<Writable>;
46-
export function initIO(outFile?: string): IO {
47-
const destDir = process.argv[3];
48-
let out: Writable;
49-
if (!destDir || !outFile) {
50-
out = new TestWritable();
51-
} else {
52-
out = fs.createWriteStream(path.resolve(process.cwd(), destDir, outFile), {
53-
flags: "w",
54-
});
55-
}
56-
57-
return {
58-
/** write to file with newline */
59-
w(s: string) {
60-
out.write(s + "\n");
61-
},
62-
63-
/** same as w() but no newline */
64-
w0(s: string) {
65-
out.write(s);
66-
},
67-
68-
copy(file) {
69-
destDir &&
70-
fs.copyFileSync(
71-
file,
72-
path.resolve(process.cwd(), destDir, path.basename(file))
73-
);
74-
},
75-
76-
out,
77-
};
78-
}

generator/schema/zod.test.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@
77
*/
88

99
import { expect, test, beforeEach } from "vitest";
10-
import type { TestWritable } from "../io";
11-
import { initIO } from "../io";
10+
import { initIO, TestWritable } from "../io";
1211
import { schemaToZod } from "./zod";
1312

14-
const io = initIO();
15-
const out = io.out as TestWritable;
13+
const out = new TestWritable();
14+
const io = initIO(out);
1615

1716
beforeEach(() => {
1817
out.clear();

0 commit comments

Comments
 (0)