Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions packages/httpapi-codegen/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ export type EffectOutputType = {
readonly import: string
}

type ResolvedEffectTypeReference = Omit<EffectTypeReference, "schema"> & { readonly ast: SchemaAST.AST }
type ResolvedEffectTypeReference = Omit<EffectTypeReference, "schema"> & {
readonly ast: SchemaAST.AST
readonly type: string | undefined
}

export class GenerationError extends Schema.TaggedError<GenerationError>()("GenerationError", {
reason: Schema.String,
Expand Down Expand Up @@ -454,7 +457,6 @@ function effectTypeReferences(input: ReadonlyArray<EffectTypeReference>) {
const asts = new Map<SchemaAST.AST, ResolvedEffectTypeReference>()
const brands = new Map<string, ResolvedEffectTypeReference>()
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)]),
)
Expand All @@ -463,6 +465,7 @@ function effectTypeReferences(input: ReadonlyArray<EffectTypeReference>) {
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)
Expand Down Expand Up @@ -499,7 +502,10 @@ function effectType(schema: Schema.Top, references: ReturnType<typeof effectType
"g",
)
if (!pattern.test(type)) continue
const reference = references.names.get(name)
// Optional/encoded occurrences can acquire a numeric suffix. Reuse the named
// type only when its definition matches; a suffix can also denote a different shape.
const candidate = references.names.get(name.replace(/_\d+$/, ""))
const reference = references.names.get(name) ?? (candidate?.type === value ? candidate : undefined)
if (reference !== undefined) {
imports.add(reference.import)
type = type.replace(pattern, reference.name)
Expand Down
46 changes: 46 additions & 0 deletions packages/httpapi-codegen/test/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,52 @@ describe("HttpApiCodegen.generate", () => {
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 }))),
Expand Down
Loading