Skip to content

Commit a268636

Browse files
fix(rust): resolve client name collisions in recursive subpackages (#9868)
* fix(rust): add clientName to `RustFilenameRegistry` for recursive clientname collision * chore:update seed
1 parent 92addbb commit a268636

File tree

189 files changed

+496
-401
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

189 files changed

+496
-401
lines changed

generators/rust/base/src/context/AbstractRustGeneratorContext.ts

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,31 @@ export abstract class AbstractRustGeneratorContext<
155155
}
156156
this.logger.debug(`Registered ${queryRequestCount} query request filenames and type names`);
157157

158+
// Priority 4: Client names (root client + all subpackage clients)
159+
let clientNameCount = 0;
160+
161+
// Register root client first
162+
const rootClientName = this.getClientName();
163+
const registeredRootClientName = this.project.filenameRegistry.registerClientName("root", rootClientName);
164+
if (registeredRootClientName !== rootClientName) {
165+
this.logger.debug(`Root client collision resolved: ${rootClientName}${registeredRootClientName}`);
166+
}
167+
clientNameCount++;
168+
169+
// Register all subpackage clients
170+
for (const [subpackageId, subpackage] of Object.entries(ir.subpackages)) {
171+
const baseClientName = `${subpackage.name.pascalCase.safeName}Client`;
172+
const registeredClientName = this.project.filenameRegistry.registerClientName(subpackageId, baseClientName);
173+
174+
if (registeredClientName !== baseClientName) {
175+
this.logger.debug(`Client collision resolved: ${baseClientName}${registeredClientName}`);
176+
}
177+
clientNameCount++;
178+
}
179+
this.logger.debug(`Registered ${clientNameCount} client names`);
180+
158181
this.logger.debug(
159-
`=== Pre-registration complete: ${schemaTypeCount + inlineRequestCount + queryRequestCount} total filenames ===`
182+
`=== Pre-registration complete: ${schemaTypeCount + inlineRequestCount + queryRequestCount} filenames, ${clientNameCount} client names ===`
160183
);
161184
}
162185

@@ -232,9 +255,19 @@ export abstract class AbstractRustGeneratorContext<
232255
}
233256

234257
/**
235-
* Get the client class name with fallback to generated default
258+
* Get the client class name using the registered name from the filename registry.
259+
* Falls back to custom config or generated default for initial registration.
236260
*/
237261
public getClientName(): string {
262+
// Try to get the registered root client name first (if project is initialized)
263+
if (this.project != null) {
264+
const registeredName = this.project.filenameRegistry.getClientNameOrUndefined("root");
265+
if (registeredName != null) {
266+
return registeredName;
267+
}
268+
}
269+
270+
// Fallback for initial registration phase (before project is created or registry is populated)
238271
return this.customConfig.clientClassName ?? `${this.ir.apiName.pascalCase.safeName}Client`;
239272
}
240273

@@ -552,17 +585,35 @@ export abstract class AbstractRustGeneratorContext<
552585
}
553586

554587
/**
555-
* Get the unique client name for a subpackage using its fernFilepath
556-
* to prevent name collisions between clients with the same name in different paths.
588+
* Get the unique client name for a subpackage using the registered name from the filename registry.
589+
* This ensures consistent naming and prevents collisions.
557590
*
558-
* @param subpackage The subpackage to generate a client name for
559-
* @returns The unique client name (e.g., "NestedNoAuthApiClient")
591+
* @param subpackage The subpackage to get the client name for
592+
* @returns The unique client name (e.g., "NestedNoAuthApiClient" or "BasicAuthClient2" if collision)
560593
*/
561594
public getUniqueClientNameForSubpackage(subpackage: {
562595
fernFilepath: { allParts: Array<{ pascalCase: { safeName: string } }> };
563596
}): string {
564-
// Use the full fernFilepath to create unique client names to prevent collisions
565-
// E.g., "nested-no-auth/api" becomes "NestedNoAuthApiClient"
597+
// Find the subpackage ID by matching fernFilepath
598+
const subpackageId = Object.entries(this.ir.subpackages).find(([, sp]) => {
599+
return (
600+
sp.fernFilepath.allParts.length === subpackage.fernFilepath.allParts.length &&
601+
sp.fernFilepath.allParts.every(
602+
(part, index) =>
603+
part.pascalCase.safeName === subpackage.fernFilepath.allParts[index]?.pascalCase.safeName
604+
)
605+
);
606+
})?.[0];
607+
608+
if (subpackageId != null) {
609+
// Use registered name if available
610+
const registeredName = this.project.filenameRegistry.getClientNameOrUndefined(subpackageId);
611+
if (registeredName != null) {
612+
return registeredName;
613+
}
614+
}
615+
616+
// Fallback to old behavior if not found (shouldn't happen in normal flow)
566617
const pathParts = subpackage.fernFilepath.allParts.map((part) => part.pascalCase.safeName);
567618
return pathParts.join("") + "Client";
568619
}

generators/rust/base/src/project/RustFilenameRegistry.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { assertDefined, SymbolRegistry } from "@fern-api/core-utils";
22

33
const FILENAME_ID_PREFIX = "filename_id:";
44
const TYPENAME_ID_PREFIX = "typename_id:";
5+
const CLIENTNAME_ID_PREFIX = "clientname_id:";
56

67
/**
78
* Registry for managing unique filenames and type names across all Rust generated files.
@@ -29,6 +30,7 @@ export class RustFilenameRegistry {
2930

3031
private readonly filenameRegistry: SymbolRegistry;
3132
private readonly typenameRegistry: SymbolRegistry;
33+
private readonly clientNameRegistry: SymbolRegistry;
3234

3335
private constructor(reservedFilenames: string[]) {
3436
// Use underscore-suffix strategy: collision → file_type → file_type_ → file_type__
@@ -42,6 +44,12 @@ export class RustFilenameRegistry {
4244
reservedSymbolNames: [],
4345
conflictResolutionStrategy: "numbered-suffix"
4446
});
47+
48+
// Client names use numbered-suffix strategy: BasicAuthClient → BasicAuthClient2 → BasicAuthClient3
49+
this.clientNameRegistry = new SymbolRegistry({
50+
reservedSymbolNames: [],
51+
conflictResolutionStrategy: "numbered-suffix"
52+
});
4553
}
4654

4755
// =====================================
@@ -120,6 +128,16 @@ export class RustFilenameRegistry {
120128
return this.typenameRegistry.registerSymbol(this.getQueryRequestTypeNameId(endpointId), [baseTypeName]);
121129
}
122130

131+
/**
132+
* Register client name for a subpackage or root client
133+
* @param clientId - Unique identifier for the client (subpackage ID or "root")
134+
* @param baseClientName - Base client name in PascalCase
135+
* @returns The registered unique client name
136+
*/
137+
public registerClientName(clientId: string, baseClientName: string): string {
138+
return this.clientNameRegistry.registerSymbol(this.getClientNameId(clientId), [baseClientName]);
139+
}
140+
123141
// =====================================
124142
// Retrieval Methods (called during file generation phase)
125143
// =====================================
@@ -196,6 +214,27 @@ export class RustFilenameRegistry {
196214
return typename;
197215
}
198216

217+
/**
218+
* Get registered client name for a subpackage or root client
219+
* @param clientId - Unique identifier for the client (subpackage ID or "root")
220+
* @returns The unique client name
221+
* @throws Error if client name not registered
222+
*/
223+
public getClientNameOrThrow(clientId: string): string {
224+
const clientName = this.clientNameRegistry.getSymbolNameById(this.getClientNameId(clientId));
225+
assertDefined(clientName, `Client name not found for client ${clientId}`);
226+
return clientName;
227+
}
228+
229+
/**
230+
* Get registered client name for a subpackage or root client (graceful)
231+
* @param clientId - Unique identifier for the client (subpackage ID or "root")
232+
* @returns The unique client name or undefined if not registered
233+
*/
234+
public getClientNameOrUndefined(clientId: string): string | undefined {
235+
return this.clientNameRegistry.getSymbolNameById(this.getClientNameId(clientId));
236+
}
237+
199238
// =====================================
200239
// Private Helper Methods
201240
// =====================================
@@ -223,4 +262,8 @@ export class RustFilenameRegistry {
223262
private getSchemaTypeTypeNameId(typeId: string): string {
224263
return `${TYPENAME_ID_PREFIX}schema_type_${typeId}`;
225264
}
265+
266+
private getClientNameId(clientId: string): string {
267+
return `${CLIENTNAME_ID_PREFIX}${clientId}`;
268+
}
226269
}

generators/rust/sdk/src/SdkGeneratorCli.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,8 @@ export class SdkGeneratorCli extends AbstractRustGeneratorCli<SdkCustomConfigSch
647647
topLevelSubpackageIds.forEach((subpackageId) => {
648648
const subpackage = context.ir.subpackages[subpackageId];
649649
if (subpackage) {
650-
const subClientName = `${subpackage.name.pascalCase.safeName}Client`;
650+
// Use registered client name from context
651+
const subClientName = context.getUniqueClientNameForSubpackage(subpackage);
651652
exports.push(subClientName);
652653
}
653654
});

generators/rust/sdk/src/SdkGeneratorContext.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ export class SdkGeneratorContext extends AbstractRustGeneratorContext<SdkCustomC
4040
.filter((subpackage) => subpackage.service != null || subpackage.hasEndpointsInTree);
4141

4242
if (subpackages.length === 1 && subpackages[0] != null) {
43-
// Single service - use the sub-client name
44-
return `${subpackages[0].name.pascalCase.safeName}Client`;
43+
// Single service - use the sub-client name (now from registry)
44+
return this.getUniqueClientNameForSubpackage(subpackages[0]);
4545
} else {
46-
// Multiple services or no subpackages - use the root client name (which now uses clientClassName config)
46+
// Multiple services or no subpackages - use the root client name (now from registry)
4747
return this.getClientName();
4848
}
4949
}

seed/rust-sdk/audiences/src/api/resources/folder_a/mod.rs

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

seed/rust-sdk/audiences/src/api/resources/folder_a/service/folder_a_service.rs

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

seed/rust-sdk/audiences/src/api/resources/folder_a/service/mod.rs

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

seed/rust-sdk/audiences/src/api/resources/folder_b/common/folder_b_common.rs

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

seed/rust-sdk/audiences/src/api/resources/folder_b/common/mod.rs

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

seed/rust-sdk/audiences/src/api/resources/folder_b/mod.rs

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)