Skip to content

Toward portability #235

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

Merged
merged 10 commits into from
Mar 28, 2024
18 changes: 0 additions & 18 deletions .vscode/launch.json

This file was deleted.

1 change: 1 addition & 0 deletions client/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import type { FetchParams } from "./http-client";
import { HttpClient, toQueryString } from "./http-client";

export type {
ApiConfig,
ApiResult,
Expand Down
50 changes: 31 additions & 19 deletions generator/client/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
* Copyright Oxide Computer Company
*/

import fs from "node:fs";
import assert from "node:assert";
import { fileURLToPath } from "node:url";
import path from "node:path";

import type { OpenAPIV3 } from "openapi-types";
import { OpenAPIV3 as O } from "openapi-types";
const HttpMethods = O.HttpMethods;
import assert from "assert";
import {
extractDoc,
pathToTemplateStr,
Expand All @@ -29,9 +33,6 @@ import {
} from "./base";
import { schemaToTypes } from "../schema/types";

const io = initIO("Api.ts");
const { w, w0, out, copy } = io;

/**
* `Error` is hard-coded into `http-client.ts` as `ErrorBody` so we can check
* and test that file statically, i.e., without doing any generation. We just
Expand Down Expand Up @@ -62,33 +63,44 @@ function checkErrorSchema(schema: Schema) {
const queryParamsType = (opId: string) => `${opId}QueryParams`;
const pathParamsType = (opId: string) => `${opId}PathParams`;

export function generateApi(spec: OpenAPIV3.Document) {
/**
* Source file is a relative path that we resolve relative to this
* file, not the CWD or package root
*/
function copyFile(sourceRelPath: string, destDirAbs: string) {
const thisFileDir = path.dirname(fileURLToPath(import.meta.url));
const sourceAbsPath = path.resolve(thisFileDir, sourceRelPath);
const destAbs = path.resolve(destDirAbs, path.basename(sourceRelPath));
fs.copyFileSync(sourceAbsPath, destAbs);
}

export function copyStaticFiles(destDir: string) {
copyFile("../../static/util.ts", destDir);
copyFile("../../static/http-client.ts", destDir);
}

export function generateApi(spec: OpenAPIV3.Document, destDir: string) {
if (!spec.components) return;

w("/* eslint-disable */\n");
const outFile = path.resolve(destDir, "Api.ts");
const out = fs.createWriteStream(outFile, { flags: "w" });
const io = initIO(out);
const { w, w0 } = io;

w(`/* eslint-disable */

w(`
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright Oxide Computer Company
*/
`);

copy("./static/util.ts");
copy("./static/http-client.ts");

w(`import type { FetchParams } from './http-client'
import { HttpClient, toQueryString } from './http-client'`);
import type { FetchParams } from './http-client'
import { HttpClient, toQueryString } from './http-client'

w(`export type {
ApiConfig,
ApiResult,
ErrorBody,
ErrorResult,
} from './http-client'
export type { ApiConfig, ApiResult, ErrorBody, ErrorResult, } from './http-client'
`);

const schemaNames = getSortedSchemas(spec);
Expand Down
12 changes: 8 additions & 4 deletions generator/client/msw-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,20 @@ import { initIO } from "../io";
import { refToSchemaName } from "../schema/base";
import { snakeToCamel, snakeToPascal } from "../util";
import { contentRef, iterPathConfig } from "./base";

const io = initIO("msw-handlers.ts");
const { w } = io;
import path from "path";
import fs from "fs";

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

export function generateMSWHandlers(spec: OpenAPIV3.Document) {
export function generateMSWHandlers(spec: OpenAPIV3.Document, destDir: string) {
if (!spec.components) return;

const outFile = path.resolve(destDir, "msw-handlers.ts");
const out = fs.createWriteStream(outFile, { flags: "w" });
const io = initIO(out);
const { w } = io;

w(`
/**
* This Source Code Form is subject to the terms of the Mozilla Public
Expand Down
12 changes: 8 additions & 4 deletions generator/client/type-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@
import type { OpenAPIV3 } from "openapi-types";
import { initIO } from "../io";
import { getSortedSchemas } from "./base";
import fs from "fs";
import path from "path";

const io = initIO("type-test.ts");
const { w } = io;

export function generateTypeTests(spec: OpenAPIV3.Document) {
export function generateTypeTests(spec: OpenAPIV3.Document, destDir: string) {
if (!spec.components) return;

const outFile = path.resolve(destDir, "type-test.ts");
const out = fs.createWriteStream(outFile, { flags: "w" });
const io = initIO(out);
const { w } = io;

const schemaNames = getSortedSchemas(spec).filter((name) => name !== "Error");

w(`
Expand Down
15 changes: 11 additions & 4 deletions generator/client/zodValidators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,22 @@ import { initIO } from "../io";
import { schemaToZod } from "../schema/zod";
import { extractDoc, processParamName, snakeToPascal } from "../util";
import { docComment, getSortedSchemas } from "./base";
import path from "path";
import fs from "fs";

const HttpMethods = OpenAPIV3.HttpMethods;

const io = initIO("validate.ts");
const { w, w0, out } = io;

export function generateZodValidators(spec: OpenAPIV3.Document) {
export function generateZodValidators(
spec: OpenAPIV3.Document,
destDir: string
) {
if (!spec.components) return;

const outFile = path.resolve(destDir, "validate.ts");
const out = fs.createWriteStream(outFile, { flags: "w" });
const io = initIO(out);
const { w, w0 } = io;

w(`/* eslint-disable */

/**
Expand Down
43 changes: 33 additions & 10 deletions generator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,42 @@
import SwaggerParser from "@apidevtools/swagger-parser";
import type { OpenAPIV3 } from "openapi-types";

import { generateApi } from "./client/api";
import { copyStaticFiles, generateApi } from "./client/api";
import { generateMSWHandlers } from "./client/msw-handlers";
import { generateTypeTests } from "./client/type-tests";
import { generateZodValidators } from "./client/zodValidators";
import { resolve } from "path";

const specFile = process.argv[2];
if (!specFile) {
throw Error("Missing specFile argument");
async function generate(specFile: string, destDir: string) {
// destination directory is resolved relative to CWD
const destDirAbs = resolve(process.cwd(), destDir);

const rawSpec = await SwaggerParser.parse(specFile);
if (!("openapi" in rawSpec) || !rawSpec.openapi.startsWith("3.0")) {
throw new Error("Only OpenAPI 3.0 is currently supported");
}

// we're not actually changing anything from rawSpec to spec, we've
// just ruled out v2 and v3.1
const spec = rawSpec as OpenAPIV3.Document;

copyStaticFiles(destDirAbs);
generateApi(spec, destDirAbs);
generateZodValidators(spec, destDirAbs);
// TODO: make conditional - we only want generated for testing purpose
generateTypeTests(spec, destDirAbs);
generateMSWHandlers(spec, destDirAbs);
}

SwaggerParser.parse(specFile).then((spec) => {
generateApi(spec as OpenAPIV3.Document);
generateZodValidators(spec as OpenAPIV3.Document);
generateTypeTests(spec as OpenAPIV3.Document);
generateMSWHandlers(spec as OpenAPIV3.Document);
});
function helpAndExit(msg: string): never {
console.log(msg);
console.log("\nUsage: gen <specFile> <destDir>");
process.exit(1);
}

const [specFile, destDir] = process.argv.slice(2);

if (!specFile) helpAndExit(`Missing <specFile>`);
if (!destDir) helpAndExit(`Missing <destdir>`);

generate(specFile, destDir);
60 changes: 15 additions & 45 deletions generator/io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,23 @@
* Copyright Oxide Computer Company
*/

import path from "path";
import fs from "fs";
import { Writable } from "stream";

export interface IO {
w: (str: string) => void;
w0: (str: string) => void;
}

// not a class because we want to destructure w and w0 in the calling code, and
// if it was a class, they would lose their 'this' on destructure
export function initIO(out: Writable) {
return {
w: (s: string) => out.write(s + "\n"),
/** same as w() but no newline */
w0: (s: string) => out.write(s),
};
}

/**
* A test stream that stores a buffer internally
*/
Expand All @@ -33,46 +46,3 @@ export class TestWritable extends Writable {
this.buffer = Buffer.from("");
}
}

export interface IO<O extends Writable = Writable> {
w: (str: string) => void;
w0: (str: string) => void;
copy: (file: string) => void;
out: O;
}

export function initIO(): IO<TestWritable>;
export function initIO(outFile: string): IO<Writable>;
export function initIO(outFile?: string): IO {
const destDir = process.argv[3];
let out: Writable;
if (!destDir || !outFile) {
out = new TestWritable();
} else {
out = fs.createWriteStream(path.resolve(process.cwd(), destDir, outFile), {
flags: "w",
});
}

return {
/** write to file with newline */
w(s: string) {
out.write(s + "\n");
},

/** same as w() but no newline */
w0(s: string) {
out.write(s);
},

copy(file) {
destDir &&
fs.copyFileSync(
file,
path.resolve(process.cwd(), destDir, path.basename(file))
);
},

out,
};
}
7 changes: 3 additions & 4 deletions generator/schema/zod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@
*/

import { expect, test, beforeEach } from "vitest";
import type { TestWritable } from "../io";
import { initIO } from "../io";
import { initIO, TestWritable } from "../io";
import { schemaToZod } from "./zod";

const io = initIO();
const out = io.out as TestWritable;
const out = new TestWritable();
const io = initIO(out);

beforeEach(() => {
out.clear();
Expand Down
Loading