diff --git a/src/commands/generate/__tests__/resources/definitions/example-service.yml b/src/commands/generate/__tests__/resources/definitions/example-service.yml index b6aacadf..72895a28 100644 --- a/src/commands/generate/__tests__/resources/definitions/example-service.yml +++ b/src/commands/generate/__tests__/resources/definitions/example-service.yml @@ -32,6 +32,7 @@ types: fields: fileSystemId: string path: string + tags: list AliasedBinary: alias: binary diff --git a/src/commands/generate/__tests__/resources/flavored-test-cases/example-service/product/createDatasetRequest.ts b/src/commands/generate/__tests__/resources/flavored-test-cases/example-service/product/createDatasetRequest.ts index 0e619a80..162fe667 100644 --- a/src/commands/generate/__tests__/resources/flavored-test-cases/example-service/product/createDatasetRequest.ts +++ b/src/commands/generate/__tests__/resources/flavored-test-cases/example-service/product/createDatasetRequest.ts @@ -1,4 +1,5 @@ export interface ICreateDatasetRequest { 'fileSystemId': string; 'path': string; + 'tags': ReadonlyArray; } diff --git a/src/commands/generate/__tests__/resources/readonly-test-cases/example-service/product/createDatasetRequest.ts b/src/commands/generate/__tests__/resources/readonly-test-cases/example-service/product/createDatasetRequest.ts index c586f0ac..e23eb876 100644 --- a/src/commands/generate/__tests__/resources/readonly-test-cases/example-service/product/createDatasetRequest.ts +++ b/src/commands/generate/__tests__/resources/readonly-test-cases/example-service/product/createDatasetRequest.ts @@ -1,4 +1,5 @@ export interface ICreateDatasetRequest { readonly 'fileSystemId': string; readonly 'path': string; + readonly 'tags': ReadonlyArray; } diff --git a/src/commands/generate/__tests__/resources/test-cases/example-service/product/createDatasetRequest.ts b/src/commands/generate/__tests__/resources/test-cases/example-service/product/createDatasetRequest.ts index 0e619a80..162fe667 100644 --- a/src/commands/generate/__tests__/resources/test-cases/example-service/product/createDatasetRequest.ts +++ b/src/commands/generate/__tests__/resources/test-cases/example-service/product/createDatasetRequest.ts @@ -1,4 +1,5 @@ export interface ICreateDatasetRequest { 'fileSystemId': string; 'path': string; + 'tags': ReadonlyArray; } diff --git a/src/commands/generate/generator.ts b/src/commands/generate/generator.ts index 7411f65f..6c8d8c15 100644 --- a/src/commands/generate/generator.ts +++ b/src/commands/generate/generator.ts @@ -21,8 +21,9 @@ import * as _ from "lodash"; import * as path from "path"; import { IServiceGenerationFlags } from "../../types/serviceGenerationFlags"; import { ITypeGenerationFlags } from "../../types/typeGenerationFlags"; +import { computeInputOnlyTypes } from "../../utils/computeInputOnlyTypes"; import { directoryNameForType } from "../../utils/fileUtils"; -import { createHashableTypeName, disassembleHashableTypeName } from "../../utils/hashingUtils"; +import { createHashableTypeName, disassembleHashableTypeName, typeNameOf } from "../../utils/hashingUtils"; import { generateError } from "./generators/generateError"; import { generateNonThrowingService } from "./generators/generateNonThrowingService"; import { generateThrowingService } from "./generators/generateThrowingService"; @@ -83,9 +84,11 @@ export async function generate( ); } - definition.types.forEach(typeDefinition => - promises.push(generateType(typeDefinition, knownTypes, simpleAst, typeGenerationFlags)), - ); + const inputOnlyTypes = computeInputOnlyTypes(definition); + definition.types.forEach(typeDefinition => { + const isInputOnly = inputOnlyTypes.has(createHashableTypeName(typeNameOf(typeDefinition))); + promises.push(generateType(typeDefinition, knownTypes, simpleAst, typeGenerationFlags, isInputOnly)); + }); definition.errors.forEach(errorDefinition => promises.push(generateError(errorDefinition, knownTypes, simpleAst, typeGenerationFlags)), diff --git a/src/commands/generate/generators/generateType.ts b/src/commands/generate/generators/generateType.ts index ad012c07..7124c085 100644 --- a/src/commands/generate/generators/generateType.ts +++ b/src/commands/generate/generators/generateType.ts @@ -48,15 +48,16 @@ export function generateType( knownTypes: Map, simpleAst: SimpleAst, typeGenerationFlags: ITypeGenerationFlags, + isInputOnly: boolean = false, ): Promise { if (ITypeDefinition.isAlias(definition)) { - return generateAlias(definition.alias, knownTypes, simpleAst, typeGenerationFlags); + return generateAlias(definition.alias, knownTypes, simpleAst, typeGenerationFlags, isInputOnly); } else if (ITypeDefinition.isEnum(definition)) { return generateEnum(definition.enum, simpleAst); } else if (ITypeDefinition.isObject(definition)) { - return generateObject(definition.object, knownTypes, simpleAst, typeGenerationFlags); + return generateObject(definition.object, knownTypes, simpleAst, typeGenerationFlags, isInputOnly); } else if (ITypeDefinition.isUnion(definition)) { - return generateUnion(definition.union, knownTypes, simpleAst, typeGenerationFlags); + return generateUnion(definition.union, knownTypes, simpleAst, typeGenerationFlags, isInputOnly); } else { throw Error("unsupported type: " + definition); } @@ -79,6 +80,7 @@ export async function generateAlias( knownTypes: Map, simpleAst: SimpleAst, typeGenerationFlags: ITypeGenerationFlags, + isInputOnly: boolean = false, ): Promise { if (isFlavorizable(definition.alias, typeGenerationFlags.flavorizedAliases)) { const fieldType = resolveTsType( @@ -86,7 +88,7 @@ export async function generateAlias( definition.typeName, knownTypes, typeGenerationFlags, - false, + isInputOnly, false, ); const sourceFile = simpleAst.createSourceFile(definition.typeName); @@ -211,6 +213,7 @@ export async function generateObject( knownTypes: Map, simpleAst: SimpleAst, typeGenerationFlags: ITypeGenerationFlags, + isInputOnly: boolean = false, ) { const properties: PropertySignatureStructure[] = []; const imports: ImportDeclarationStructure[] = []; @@ -220,7 +223,7 @@ export async function generateObject( definition.typeName, knownTypes, typeGenerationFlags, - false, + isInputOnly, false, ); const docs = addDeprecatedToDocs(fieldDefinition); @@ -265,9 +268,16 @@ export async function generateUnion( knownTypes: Map, simpleAst: SimpleAst, typeGenerationFlags: ITypeGenerationFlags, + isInputOnly: boolean = false, ) { const unionTsType = "I" + definition.typeName.name; - const unionSourceFileInput = processUnionMembers(unionTsType, definition, knownTypes, typeGenerationFlags); + const unionSourceFileInput = processUnionMembers( + unionTsType, + definition, + knownTypes, + typeGenerationFlags, + isInputOnly, + ); const sourceFile = simpleAst.createSourceFile(definition.typeName); if (unionSourceFileInput.imports.length !== 0) { @@ -339,6 +349,7 @@ function processUnionMembers( definition: IUnionDefinition, knownTypes: Map, typeGenerationFlags: ITypeGenerationFlags, + isInputOnly: boolean, ) { const imports: ImportDeclarationStructure[] = []; const visitorProperties: PropertySignatureStructure[] = []; @@ -353,7 +364,7 @@ function processUnionMembers( definition.typeName, knownTypes, typeGenerationFlags, - false, + isInputOnly, false, ); imports.push(...resolveImports(fieldDefinition.type, definition.typeName, knownTypes, typeGenerationFlags)); diff --git a/src/utils/computeInputOnlyTypes.ts b/src/utils/computeInputOnlyTypes.ts new file mode 100644 index 00000000..5cc66263 --- /dev/null +++ b/src/utils/computeInputOnlyTypes.ts @@ -0,0 +1,99 @@ +/** + * @license + * Copyright 2026 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { IConjureDefinition, IType, ITypeDefinition } from "conjure-api"; +import { createHashableTypeName, typeNameOf } from "./hashingUtils"; + +/** + * Returns the set of hashable type names that are reachable only from endpoint input positions + * (endpoint args, transitively through fields/members/aliases). These types are emitted with + * `isParameterType=true` so list/set fields become `ReadonlyArray`. + * + * Output positions include endpoint return values and any errors (declared or referenced by + * endpoints). A type used in both input and output positions is excluded. + */ +export function computeInputOnlyTypes(definition: IConjureDefinition): Set { + const knownTypes = new Map(); + for (const t of definition.types) { + knownTypes.set(createHashableTypeName(typeNameOf(t)), t); + } + + const inputUsed = new Set(); + const outputUsed = new Set(); + + const visit = (type: IType, set: Set): void => { + switch (type.type) { + case "primitive": + return; + case "list": + visit(type.list.itemType, set); + return; + case "set": + visit(type.set.itemType, set); + return; + case "map": + visit(type.map.keyType, set); + visit(type.map.valueType, set); + return; + case "optional": + visit(type.optional.itemType, set); + return; + case "external": + visit(type.external.fallback, set); + return; + case "reference": { + const k = createHashableTypeName(type.reference); + if (set.has(k)) return; + set.add(k); + const def = knownTypes.get(k); + if (def == null) return; + switch (def.type) { + case "object": + def.object.fields.forEach(f => visit(f.type, set)); + return; + case "union": + def.union.union.forEach(f => visit(f.type, set)); + return; + case "alias": + visit(def.alias.alias, set); + return; + case "enum": + return; + } + } + } + }; + + for (const svc of definition.services) { + for (const ep of svc.endpoints) { + ep.args.forEach(arg => visit(arg.type, inputUsed)); + if (ep.returns != null) visit(ep.returns, outputUsed); + } + } + + // All errors live in output positions: their fields are read by the caller when an error is thrown. + for (const errDef of definition.errors) { + errDef.safeArgs.forEach(arg => visit(arg.type, outputUsed)); + errDef.unsafeArgs.forEach(arg => visit(arg.type, outputUsed)); + } + + const result = new Set(); + inputUsed.forEach(k => { + if (!outputUsed.has(k)) result.add(k); + }); + return result; +} diff --git a/src/utils/hashingUtils.ts b/src/utils/hashingUtils.ts index 315f73ab..aadf469e 100644 --- a/src/utils/hashingUtils.ts +++ b/src/utils/hashingUtils.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { ITypeName } from "conjure-api"; +import { ITypeDefinition, ITypeName } from "conjure-api"; const FIELD_SEPARATOR = "|-|"; @@ -27,3 +27,16 @@ export const disassembleHashableTypeName = (hash: string): ITypeName => { const [packageName, name] = hash.split(FIELD_SEPARATOR); return { package: packageName, name }; }; + +export const typeNameOf = (definition: ITypeDefinition): ITypeName => { + switch (definition.type) { + case "alias": + return definition.alias.typeName; + case "enum": + return definition.enum.typeName; + case "object": + return definition.object.typeName; + case "union": + return definition.union.typeName; + } +};