diff --git a/packages/httpapi-codegen/src/index.ts b/packages/httpapi-codegen/src/index.ts index e686a8be4c6f..d945ece0b74d 100644 --- a/packages/httpapi-codegen/src/index.ts +++ b/packages/httpapi-codegen/src/index.ts @@ -46,7 +46,10 @@ export type EffectOutputType = { readonly import: string } -type ResolvedEffectTypeReference = Omit & { readonly ast: SchemaAST.AST } +type ResolvedEffectTypeReference = Omit & { + readonly ast: SchemaAST.AST + readonly type: string | undefined +} export class GenerationError extends Schema.TaggedError()("GenerationError", { reason: Schema.String, @@ -454,7 +457,6 @@ function effectTypeReferences(input: ReadonlyArray) { const asts = new Map() const brands = new Map() for (const reference of input) { - const value = { name: reference.name, import: reference.import, ast: reference.schema.ast } const document = SchemaRepresentation.toCodeDocument( SchemaRepresentation.toRepresentations([codegenAst(Schema.toType(reference.schema).ast)]), ) @@ -463,6 +465,7 @@ function effectTypeReferences(input: ReadonlyArray) { name === undefined ? undefined : (document.references.nonRecursives.find((item) => item.$ref === name)?.code.Type ?? name) + const value = { name: reference.name, import: reference.import, ast: reference.schema.ast, type } if (type?.includes("Brand.Brand<") && !brands.has(type)) brands.set(type, value) if (SchemaAST.resolveIdentifier(reference.schema.ast) !== undefined || type?.includes("Brand.Brand<")) { asts.set(reference.schema.ast, value) @@ -499,7 +502,10 @@ function effectType(schema: Schema.Top, references: ReturnType { expect(source).not.toContain("@example/api") }) + test("preserves named Effect references across optional schema occurrences", () => { + const State = Schema.Record(Schema.String, Schema.Unknown).annotate({ identifier: "Message.State" }) + const output = emitEffectShape( + compileContract( + api( + HttpApiEndpoint.get("get", "/session", { + success: Schema.Struct({ + encoded: Schema.toEncoded(Schema.Struct({ state: Schema.optionalKey(State) })), + optional: Schema.optional(State), + }), + }), + ), + ), + { + typeReferences: [ + { schema: State, name: "Message.State", import: 'import type { Message } from "@example/schema/message"' }, + ], + }, + ) + const source = output.files[0]?.content + expect(source).toContain('readonly "state"?: Message.State') + expect(source).toContain('readonly "optional"?: Message.State | undefined') + }) + + test("does not reuse named Effect references for different suffixed shapes", () => { + const State = Schema.Record(Schema.String, Schema.Unknown).annotate({ identifier: "Message.State" }) + const Different = Schema.Record(Schema.String, Schema.Number).annotate({ identifier: "Message.State" }) + const output = emitEffectShape( + compileContract( + api( + HttpApiEndpoint.get("get", "/session", { + success: Schema.Struct({ original: State, different: Different }), + }), + ), + ), + { + typeReferences: [ + { schema: State, name: "Message.State", import: 'import type { Message } from "@example/schema/message"' }, + ], + }, + ) + const source = output.files[0]?.content + expect(source).toContain('readonly "original": Message.State') + expect(source).toContain('readonly "different": ({ readonly [x: string]: number })') + }) + test("allows composed Effect outputs to use an authoritative named type", () => { const output = emitEffectShape( compileContract(api(HttpApiEndpoint.get("events", "/event", { success: Schema.Unknown }))),