From c33e7f3c42ae08ad14fec61672911558f3cd6ae8 Mon Sep 17 00:00:00 2001 From: MarkXian Date: Thu, 23 Jul 2026 12:39:14 +0800 Subject: [PATCH] fix(openapi): return fresh cached specs --- .changeset/fix-openapi-from-api-cache-copy.md | 5 ++++ .../effect/src/unstable/httpapi/OpenApi.ts | 23 +++++++++++++++++-- .../test/unstable/httpapi/OpenApi.test.ts | 20 ++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-openapi-from-api-cache-copy.md diff --git a/.changeset/fix-openapi-from-api-cache-copy.md b/.changeset/fix-openapi-from-api-cache-copy.md new file mode 100644 index 00000000000..68a5c04f448 --- /dev/null +++ b/.changeset/fix-openapi-from-api-cache-copy.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Return fresh OpenAPI specs from cached `OpenApi.fromApi` calls. diff --git a/packages/effect/src/unstable/httpapi/OpenApi.ts b/packages/effect/src/unstable/httpapi/OpenApi.ts index 31a709ef5af..e166b4623cd 100644 --- a/packages/effect/src/unstable/httpapi/OpenApi.ts +++ b/packages/effect/src/unstable/httpapi/OpenApi.ts @@ -212,6 +212,25 @@ export const annotations: ( const apiCache = new WeakMap() +const cloneOpenAPISpec = (value: A): A => { + if (Array.isArray(value)) { + return value.map(cloneOpenAPISpec) as A + } + if (value !== null && typeof value === "object") { + const out: Record = {} + for (const key of Object.keys(value)) { + Object.defineProperty(out, key, { + value: cloneOpenAPISpec((value as Record)[key]), + enumerable: true, + configurable: true, + writable: true + }) + } + return out as A + } + return value +} + /** * This function checks if a given tag exists within the provided context. If * the tag is present, it retrieves the associated value and applies the given @@ -253,7 +272,7 @@ export function fromApi { + it("returns fresh spec instances when using the cache", () => { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("get", "/resource") + ) + ) + + const first = OpenApi.fromApi(Api) + first.info.title = "mutated" + first.paths["/resource"]!.get!.summary = "mutated" + + const second = OpenApi.fromApi(Api) + + assert.notStrictEqual(first, second) + assert.notStrictEqual(first.info, second.info) + assert.notStrictEqual(first.paths["/resource"]!.get, second.paths["/resource"]!.get) + assert.strictEqual(second.info.title, "Api") + assert.isUndefined(second.paths["/resource"]!.get!.summary) + }) + it("preserves every declared payload content type for normalized equivalents", () => { const profileA = "Application/Vnd.Effect+JSON; Profile=A" const profileB = "application/vnd.effect+json; profile=b"