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
67 changes: 59 additions & 8 deletions generators/rust/base/src/context/AbstractRustGeneratorContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,31 @@ export abstract class AbstractRustGeneratorContext<
}
this.logger.debug(`Registered ${queryRequestCount} query request filenames and type names`);

// Priority 4: Client names (root client + all subpackage clients)
let clientNameCount = 0;

// Register root client first
const rootClientName = this.getClientName();
const registeredRootClientName = this.project.filenameRegistry.registerClientName("root", rootClientName);
if (registeredRootClientName !== rootClientName) {
this.logger.debug(`Root client collision resolved: ${rootClientName} → ${registeredRootClientName}`);
}
clientNameCount++;

// Register all subpackage clients
for (const [subpackageId, subpackage] of Object.entries(ir.subpackages)) {
const baseClientName = `${subpackage.name.pascalCase.safeName}Client`;
const registeredClientName = this.project.filenameRegistry.registerClientName(subpackageId, baseClientName);

if (registeredClientName !== baseClientName) {
this.logger.debug(`Client collision resolved: ${baseClientName} → ${registeredClientName}`);
}
clientNameCount++;
}
this.logger.debug(`Registered ${clientNameCount} client names`);

this.logger.debug(
`=== Pre-registration complete: ${schemaTypeCount + inlineRequestCount + queryRequestCount} total filenames ===`
`=== Pre-registration complete: ${schemaTypeCount + inlineRequestCount + queryRequestCount} filenames, ${clientNameCount} client names ===`
);
}

Expand Down Expand Up @@ -232,9 +255,19 @@ export abstract class AbstractRustGeneratorContext<
}

/**
* Get the client class name with fallback to generated default
* Get the client class name using the registered name from the filename registry.
* Falls back to custom config or generated default for initial registration.
*/
public getClientName(): string {
// Try to get the registered root client name first (if project is initialized)
if (this.project != null) {
const registeredName = this.project.filenameRegistry.getClientNameOrUndefined("root");
if (registeredName != null) {
return registeredName;
}
}

// Fallback for initial registration phase (before project is created or registry is populated)
return this.customConfig.clientClassName ?? `${this.ir.apiName.pascalCase.safeName}Client`;
}

Expand Down Expand Up @@ -552,17 +585,35 @@ export abstract class AbstractRustGeneratorContext<
}

/**
* Get the unique client name for a subpackage using its fernFilepath
* to prevent name collisions between clients with the same name in different paths.
* Get the unique client name for a subpackage using the registered name from the filename registry.
* This ensures consistent naming and prevents collisions.
*
* @param subpackage The subpackage to generate a client name for
* @returns The unique client name (e.g., "NestedNoAuthApiClient")
* @param subpackage The subpackage to get the client name for
* @returns The unique client name (e.g., "NestedNoAuthApiClient" or "BasicAuthClient2" if collision)
*/
public getUniqueClientNameForSubpackage(subpackage: {
fernFilepath: { allParts: Array<{ pascalCase: { safeName: string } }> };
}): string {
// Use the full fernFilepath to create unique client names to prevent collisions
// E.g., "nested-no-auth/api" becomes "NestedNoAuthApiClient"
// Find the subpackage ID by matching fernFilepath
const subpackageId = Object.entries(this.ir.subpackages).find(([, sp]) => {
return (
sp.fernFilepath.allParts.length === subpackage.fernFilepath.allParts.length &&
sp.fernFilepath.allParts.every(
(part, index) =>
part.pascalCase.safeName === subpackage.fernFilepath.allParts[index]?.pascalCase.safeName
)
);
})?.[0];

if (subpackageId != null) {
// Use registered name if available
const registeredName = this.project.filenameRegistry.getClientNameOrUndefined(subpackageId);
if (registeredName != null) {
return registeredName;
}
}

// Fallback to old behavior if not found (shouldn't happen in normal flow)
const pathParts = subpackage.fernFilepath.allParts.map((part) => part.pascalCase.safeName);
return pathParts.join("") + "Client";
}
Expand Down
43 changes: 43 additions & 0 deletions generators/rust/base/src/project/RustFilenameRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { assertDefined, SymbolRegistry } from "@fern-api/core-utils";

const FILENAME_ID_PREFIX = "filename_id:";
const TYPENAME_ID_PREFIX = "typename_id:";
const CLIENTNAME_ID_PREFIX = "clientname_id:";

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

private readonly filenameRegistry: SymbolRegistry;
private readonly typenameRegistry: SymbolRegistry;
private readonly clientNameRegistry: SymbolRegistry;

private constructor(reservedFilenames: string[]) {
// Use underscore-suffix strategy: collision → file_type → file_type_ → file_type__
Expand All @@ -42,6 +44,12 @@ export class RustFilenameRegistry {
reservedSymbolNames: [],
conflictResolutionStrategy: "numbered-suffix"
});

// Client names use numbered-suffix strategy: BasicAuthClient → BasicAuthClient2 → BasicAuthClient3
this.clientNameRegistry = new SymbolRegistry({
reservedSymbolNames: [],
conflictResolutionStrategy: "numbered-suffix"
});
}

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

/**
* Register client name for a subpackage or root client
* @param clientId - Unique identifier for the client (subpackage ID or "root")
* @param baseClientName - Base client name in PascalCase
* @returns The registered unique client name
*/
public registerClientName(clientId: string, baseClientName: string): string {
return this.clientNameRegistry.registerSymbol(this.getClientNameId(clientId), [baseClientName]);
}

// =====================================
// Retrieval Methods (called during file generation phase)
// =====================================
Expand Down Expand Up @@ -196,6 +214,27 @@ export class RustFilenameRegistry {
return typename;
}

/**
* Get registered client name for a subpackage or root client
* @param clientId - Unique identifier for the client (subpackage ID or "root")
* @returns The unique client name
* @throws Error if client name not registered
*/
public getClientNameOrThrow(clientId: string): string {
const clientName = this.clientNameRegistry.getSymbolNameById(this.getClientNameId(clientId));
assertDefined(clientName, `Client name not found for client ${clientId}`);
return clientName;
}

/**
* Get registered client name for a subpackage or root client (graceful)
* @param clientId - Unique identifier for the client (subpackage ID or "root")
* @returns The unique client name or undefined if not registered
*/
public getClientNameOrUndefined(clientId: string): string | undefined {
return this.clientNameRegistry.getSymbolNameById(this.getClientNameId(clientId));
}

// =====================================
// Private Helper Methods
// =====================================
Expand Down Expand Up @@ -223,4 +262,8 @@ export class RustFilenameRegistry {
private getSchemaTypeTypeNameId(typeId: string): string {
return `${TYPENAME_ID_PREFIX}schema_type_${typeId}`;
}

private getClientNameId(clientId: string): string {
return `${CLIENTNAME_ID_PREFIX}${clientId}`;
}
}
3 changes: 2 additions & 1 deletion generators/rust/sdk/src/SdkGeneratorCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,8 @@ export class SdkGeneratorCli extends AbstractRustGeneratorCli<SdkCustomConfigSch
topLevelSubpackageIds.forEach((subpackageId) => {
const subpackage = context.ir.subpackages[subpackageId];
if (subpackage) {
const subClientName = `${subpackage.name.pascalCase.safeName}Client`;
// Use registered client name from context
const subClientName = context.getUniqueClientNameForSubpackage(subpackage);
exports.push(subClientName);
}
});
Expand Down
6 changes: 3 additions & 3 deletions generators/rust/sdk/src/SdkGeneratorContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ export class SdkGeneratorContext extends AbstractRustGeneratorContext<SdkCustomC
.filter((subpackage) => subpackage.service != null || subpackage.hasEndpointsInTree);

if (subpackages.length === 1 && subpackages[0] != null) {
// Single service - use the sub-client name
return `${subpackages[0].name.pascalCase.safeName}Client`;
// Single service - use the sub-client name (now from registry)
return this.getUniqueClientNameForSubpackage(subpackages[0]);
} else {
// Multiple services or no subpackages - use the root client name (which now uses clientClassName config)
// Multiple services or no subpackages - use the root client name (now from registry)
return this.getClientName();
}
}
Expand Down
6 changes: 3 additions & 3 deletions seed/rust-sdk/audiences/src/api/resources/folder_a/mod.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion seed/rust-sdk/audiences/src/api/resources/folder_b/mod.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion seed/rust-sdk/audiences/src/api/resources/folder_c/mod.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions seed/rust-sdk/audiences/src/api/resources/folder_d/mod.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion seed/rust-sdk/basic-auth/src/api/mod.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions seed/rust-sdk/basic-auth/src/api/resources/mod.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading