From 994a2ceab1ab283fa9a83cb83e7dfb3f5b72fde2 Mon Sep 17 00:00:00 2001 From: Patrick McElhaney Date: Sun, 20 Oct 2024 22:34:57 -0400 Subject: [PATCH] change async requirementAt to sync getRequirement --- src/typescript-generator/generate.js | 6 +- src/typescript-generator/requirement.js | 2 +- src/typescript-generator/specification.js | 63 ++++------- test/typescript-generator/coder.test.js | 10 +- test/typescript-generator/integration.test.js | 33 +++--- .../operation-type-coder.test.js | 10 +- .../specification.test.js | 102 ++++-------------- 7 files changed, 70 insertions(+), 156 deletions(-) diff --git a/src/typescript-generator/generate.js b/src/typescript-generator/generate.js index 77ebd20b..37be6b3f 100644 --- a/src/typescript-generator/generate.js +++ b/src/typescript-generator/generate.js @@ -38,7 +38,7 @@ async function buildCacheDirectory(destination) { async function getPathsFromSpecification(specification) { try { - return (await specification.requirementAt("#/paths")) ?? new Set(); + return specification.getRequirement("#/paths") ?? new Set(); } catch (error) { process.stderr.write( `Could not find #/paths in the specification.\n${error}\n`, @@ -62,7 +62,7 @@ export async function generate( debug("creating specification from %s", source); - const specification = new Specification(source); + const specification = await Specification.fromFile(source); debug("created specification: $o", specification); @@ -72,7 +72,7 @@ export async function generate( debug("got %i paths", paths.size); - const securityRequirement = await specification.requirementAt( + const securityRequirement = specification.getRequirement( "#/components/securitySchemes", ); diff --git a/src/typescript-generator/requirement.js b/src/typescript-generator/requirement.js index 98b7b3dc..56079fa2 100644 --- a/src/typescript-generator/requirement.js +++ b/src/typescript-generator/requirement.js @@ -10,7 +10,7 @@ export class Requirement { } reference() { - return this.specification.requirementAt(this.data.$ref, this.url); + return this.specification.getRequirement(this.data.$ref); } has(item) { diff --git a/src/typescript-generator/specification.js b/src/typescript-generator/specification.js index 054092b9..7457a1e1 100644 --- a/src/typescript-generator/specification.js +++ b/src/typescript-generator/specification.js @@ -5,6 +5,8 @@ import yaml from "js-yaml"; import { readFile } from "../util/read-file.js"; import { Requirement } from "./requirement.js"; +import { bundle } from "@apidevtools/json-schema-ref-parser"; +import { url } from "node:inspector"; const debug = createDebug("counterfact:typescript-generator:specification"); @@ -18,56 +20,29 @@ const EMPTY_OPENAPI = `{ }`; export class Specification { - constructor(rootUrl) { + constructor(rootRequirement) { this.cache = new Map(); - this.rootUrl = rootUrl; + this.rootUrl = rootRequirement; + this.rootRequirement = rootRequirement; } - async requirementAt(url, fromUrl = "") { - debug("getting requirement at %s from %s", url, fromUrl); - - const [file, path] = url.split("#"); - const filePath = nodePath - .join(fromUrl.split("#").at(0), file) - .replaceAll("\\", "/") - // eslint-disable-next-line regexp/prefer-named-capture-group - .replace(/:\/([^/])/u, "://$1"); - const fileUrl = filePath === "." ? this.rootUrl : filePath; - - debug("reading specification at %s", fileUrl); - - const data = await this.loadFile(fileUrl); - - debug("done reading specification", fileUrl); - - const rootRequirement = new Requirement(data, `${fileUrl}#`, this); - - return rootRequirement.select(path.slice(1)); + static async fromFile(urlOrPath) { + const specification = new Specification(); + await specification.load(urlOrPath); + return specification; } - async loadFile(urlOrPath) { - debug("loading file %s", urlOrPath); - - if (this.cache.has(urlOrPath)) { - debug("cache hit"); + getRequirement(url) { + debug("getting requirement at %s", url); - return this.cache.get(urlOrPath); - } - - debug("cache miss, reading file at %s", urlOrPath); - - const source = - urlOrPath === "_" ? EMPTY_OPENAPI : await readFile(urlOrPath, "utf8"); - - debug("read file"); - debug("parsing YAML"); - - const data = await yaml.load(source); - - debug("parsed YAML: %o", data); - this.cache.set(urlOrPath, data); - this.rootRequirement = new Requirement(data, `${urlOrPath}#`, this); + return this.rootRequirement.select(url.slice(2)); + } - return data; + async load(urlOrPath) { + this.rootRequirement = new Requirement( + await bundle(urlOrPath), + urlOrPath, + this, + ); } } diff --git a/test/typescript-generator/coder.test.js b/test/typescript-generator/coder.test.js index c412597f..f493de4c 100644 --- a/test/typescript-generator/coder.test.js +++ b/test/typescript-generator/coder.test.js @@ -83,11 +83,13 @@ describe("a Coder", () => { }); it("when asked to delegate a requirement that is a $ref, looks up the $ref and returns another coder of the same type with an inline requirement", async () => { - const specification = new Specification("openapi.yaml"); + const specification = new Specification( + new Requirement({ + components: { schemas: { Person: { type: "string" } } }, + }), + ); - specification.cache.set("openapi.yaml", { - components: { schemas: { Person: { type: "string" } } }, - }); + specification.cache.set("openapi.yaml"); class DelegatingCoder extends Coder {} diff --git a/test/typescript-generator/integration.test.js b/test/typescript-generator/integration.test.js index d710f9b7..13016484 100644 --- a/test/typescript-generator/integration.test.js +++ b/test/typescript-generator/integration.test.js @@ -3,29 +3,28 @@ import { describe, expect, it } from "@jest/globals"; import { Coder } from "../../src/typescript-generator/coder.js"; import { Repository } from "../../src/typescript-generator/repository.js"; import { Specification } from "../../src/typescript-generator/specification.js"; +import { Requirement } from "../../src/typescript-generator/requirement.js"; describe("integration Test", () => { it("writes some code", async () => { - const specification = new Specification(); - - specification.cache.set("openapi.yaml", { - paths: { - "/accounts": { - get: {}, - post: {}, - }, - - "/accounts/{id}": { - get: {}, - put: {}, + const specification = new Specification( + new Requirement({ + paths: { + "/accounts": { + get: {}, + post: {}, + }, + + "/accounts/{id}": { + get: {}, + put: {}, + }, }, - }, - }); + }), + ); const repository = new Repository(); - const requirement = await specification.requirementAt( - "openapi.yaml#/paths", - ); + const requirement = await specification.getRequirement("#/paths"); class OperationCoder extends Coder { *names() { diff --git a/test/typescript-generator/operation-type-coder.test.js b/test/typescript-generator/operation-type-coder.test.js index 55fa81a3..e83a93f9 100644 --- a/test/typescript-generator/operation-type-coder.test.js +++ b/test/typescript-generator/operation-type-coder.test.js @@ -228,11 +228,11 @@ describe("an OperationTypeCoder", () => { }); it("generates a complex post operation (OpenAPI 2 with produces at the root)", async () => { - const specification = new Specification(); - - specification.rootRequirement = new Requirement({ - produces: ["application/json"], - }); + const specification = new Specification( + new Requirement({ + produces: ["application/json"], + }), + ); const requirement = new Requirement( { diff --git a/test/typescript-generator/specification.test.js b/test/typescript-generator/specification.test.js index b9c03c62..60161bdb 100644 --- a/test/typescript-generator/specification.test.js +++ b/test/typescript-generator/specification.test.js @@ -12,11 +12,11 @@ describe("a Specification", () => { await withTemporaryFiles( { "openapi.yaml": "hello:\n world" }, async (temporaryDirectory, { path }) => { - const specification = new Specification(); + const specification = await Specification.fromFile( + path("openapi.yaml"), + ); - await specification.loadFile(path("openapi.yaml")); - - expect(specification.cache.get(path("openapi.yaml"))).toStrictEqual({ + expect(specification.rootRequirement.data).toStrictEqual({ hello: "world", }); }, @@ -27,13 +27,11 @@ describe("a Specification", () => { await withTemporaryFiles( { "openapi.yaml": "hello:\n world" }, async (temporaryDirectory, { path }) => { - const specification = new Specification(); - const { href } = pathToFileURL(path("openapi.yaml")); - await specification.loadFile(href); + const specification = await Specification.fromFile(href); - expect(specification.cache.get(href)).toStrictEqual({ + expect(specification.rootRequirement.data).toStrictEqual({ hello: "world", }); }, @@ -49,12 +47,9 @@ describe("a Specification", () => { const server = app.listen(0); const url = `http://localhost:${server.address().port}/`; + const specification = await Specification.fromFile(url); - const specification = new Specification(); - - await specification.loadFile(url); - - expect(specification.cache.get(url)).toStrictEqual({ + expect(specification.rootRequirement.data).toStrictEqual({ hello: "world", }); @@ -69,87 +64,30 @@ describe("a Specification", () => { }); }); - it("returns a requirement for a URL", async () => { - const specification = new Specification(); - - specification.cache.set( - "openapi.yaml", - Promise.resolve({ - components: { - schemas: { - Person: { - properties: { - name: { type: "string" }, - phone: { type: "string" }, - }, - - type: "object", - }, - }, - }, - }), - ); - - const person = new Requirement( - { - properties: { - name: { type: "string" }, - phone: { type: "string" }, - }, - - type: "object", + it("looks for requirements in the root document if no path is provided", () => { + const person = { + properties: { + name: { type: "string" }, + phone: { type: "string" }, }, - "openapi.yaml#/components/schemas/Person", - specification, - ); - - const requirement = await specification.requirementAt( - "openapi.yaml#/components/schemas/Person", - ); - - expect(requirement).toStrictEqual(person); - }); - - it("looks for requirements in the root document if no path is provided", async () => { - const specification = new Specification("openapi.yaml"); + type: "object", + }; - specification.cache.set( - "openapi.yaml", - Promise.resolve({ + const specification = new Specification( + new Requirement({ components: { schemas: { - Person: { - properties: { - name: { type: "string" }, - phone: { type: "string" }, - }, - - type: "object", - }, + Person: person, }, }, }), ); - const person = new Requirement( - { - properties: { - name: { type: "string" }, - phone: { type: "string" }, - }, - - type: "object", - }, - - "openapi.yaml#/components/schemas/Person", - specification, - ); - - const requirement = await specification.requirementAt( + const requirement = specification.getRequirement( "#/components/schemas/Person", ); - expect(requirement).toStrictEqual(person); + expect(requirement.data).toStrictEqual(person); }); });