Skip to content
Open
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 2 additions & 1 deletion packages/typespec-rust/.scripts/tspcompile.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ function generate(crate, input, outputDir, additionalArgs) {
options.push(`--option="@azure-tools/typespec-rust.crate-name=${crate}"`);
options.push(`--option="@azure-tools/typespec-rust.crate-version=0.1.0"`);
options.push(`--option="@azure-tools/typespec-rust.emitter-output-dir=${fullOutputDir}"`);
//options.push(`--option="@azure-tools/typespec-rust.overwrite-lib-rs=true"`);
const command = `node ${compiler} compile ${input} --emit=${pkgRoot} ${options.join(' ')} ${additionalArgs.join(' ')}`;
if (switches.includes('--verbose')) {
console.log(command);
Expand All @@ -261,7 +262,7 @@ function generate(crate, input, outputDir, additionalArgs) {
const maxRmRetries = 4;
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
for (let attempt = 0; attempt < maxRmRetries; ++attempt) {
const rmPath = path.join(fullOutputDir, 'src', 'generated')
const rmPath = path.join(fullOutputDir, 'src', 'generated');
try {
fs.rmSync(rmPath, { force: true, recursive: true });
break;
Expand Down
8 changes: 8 additions & 0 deletions packages/typespec-rust/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Release History

## 0.36.0 (unreleased)

### Breaking Changes

* TypeSpec namespaces are now honored and emitted as sub-modules.
* The root namespace is selected from the first defined client. All content in the root namespace is exported as `crate_name::clients::*` and `crate_name::models::*`.
* If there are no defined clients, the the root namespace is selected from a non-core model type.

## 0.35.0 (2026-02-13)

### Breaking Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/typespec-rust/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@azure-tools/typespec-rust",
"version": "0.35.0",
"version": "0.36.0",
"description": "TypeSpec emitter for Rust SDKs",
"type": "module",
"packageManager": "pnpm@10.10.0",
Expand Down
42 changes: 25 additions & 17 deletions packages/typespec-rust/src/codegen/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ export interface ClientModules {
modules: Array<helpers.Module>;

/** the client method options module */
options: helpers.Module;
options?: helpers.Module;
}

/**
* emits the content for all client files
*
* @param crate the crate for which to emit clients
* @returns client content or undefined if the crate contains no clients
* @param module the module for which to emit clients
* @returns client content or undefined if the module contains no clients
*/
export function emitClients(crate: rust.Crate): ClientModules | undefined {
if (crate.clients.length === 0) {
export function emitClients(module: rust.ModuleContainer): ClientModules | undefined {
if (module.clients.length === 0) {
return undefined;
}

Expand All @@ -49,8 +49,8 @@ export function emitClients(crate: rust.Crate): ClientModules | undefined {
const clientModules = new Array<helpers.Module>();

// emit the clients, one file per client
for (const client of crate.clients) {
const use = new Use('clients');
for (const client of module.clients) {
const use = new Use(module, 'clients');
const indent = new helpers.indentation();

let body = helpers.formatDocComment(client.docs);
Expand Down Expand Up @@ -196,6 +196,8 @@ export function emitClients(crate: rust.Crate): ClientModules | undefined {
body += `${indent.push().get()}&self.${client.endpoint.name}\n`;
body += `${indent.pop().get()}}\n\n`;

const crate = helpers.getCrate(module);

for (let i = 0; i < client.methods.length; ++i) {
const method = client.methods[i];
const returnType = helpers.getTypeDeclaration(method.returns);
Expand Down Expand Up @@ -295,7 +297,8 @@ export function emitClients(crate: rust.Crate): ClientModules | undefined {
// add using for method_options as required
for (const method of client.methods) {
if (method.kind !== 'clientaccessor') {
use.add('crate::generated::models', method.options.type.name);
// client method options types are always in the same module as their client method
use.add(`${utils.buildImportPath(client.module, client.module)}::models`, method.options.type.name);
}
}

Expand All @@ -313,17 +316,17 @@ export function emitClients(crate: rust.Crate): ClientModules | undefined {

return {
modules: clientModules,
options: getMethodOptions(crate),
options: getMethodOptions(module),
};
}

function getMethodOptions(crate: rust.Crate): helpers.Module {
const use = new Use('modelsOther');
function getMethodOptions(module: rust.ModuleContainer): helpers.Module | undefined {
const use = new Use(module, 'modelsOther');
const indent = new helpers.indentation();
const visTracker = new helpers.VisibilityTracker();

let body = '';
for (const client of crate.clients) {
for (const client of module.clients) {
for (let i = 0; i < client.methods.length; ++i) {
const method = client.methods[i];
if (method.kind === 'clientaccessor') {
Expand Down Expand Up @@ -377,6 +380,11 @@ function getMethodOptions(crate: rust.Crate): helpers.Module {
}
}

if (body === '') {
// client is top-level only, no methods just accessors
return undefined;
}

let content = helpers.contentPreamble();
content += use.text();
content += body;
Expand Down Expand Up @@ -516,11 +524,11 @@ function getMethodParamsCountAndSig(method: rust.MethodType, use: Use): { count:
* returns documentation for header trait access if the method has response headers.
*
* @param indent the current indentation level
* @param crate the crate to which method belongs
* @param module the module to which method belongs
* @param method the method for which to generate header trait documentation
* @returns the header trait documentation or empty string if not applicable
*/
function getHeaderTraitDocComment(indent: helpers.indentation, crate: rust.Crate, method: ClientMethod): string {
function getHeaderTraitDocComment(indent: helpers.indentation, module: rust.ModuleContainer, method: ClientMethod): string {
if (!method.responseHeaders) {
return '';
}
Expand All @@ -543,17 +551,17 @@ function getHeaderTraitDocComment(indent: helpers.indentation, crate: rust.Crate
headerDocs += `${indent.get()}/// The returned [${helpers.wrapInBackTicks(returnType)}](azure_core::http::${returnType}) implements the [${helpers.wrapInBackTicks(traitName)}] trait, which provides\n`;
headerDocs += `${indent.get()}/// access to response headers. For example:\n`;
headerDocs += `${indent.get()}///\n`;
headerDocs += emitHeaderTraitDocExample(crate.name, method.responseHeaders, indent);
headerDocs += emitHeaderTraitDocExample(method.responseHeaders, indent);
headerDocs += `${indent.get()}///\n`;
headerDocs += `${indent.get()}/// ### Available headers\n`;

// List all available headers
for (const header of method.responseHeaders.headers) {
headerDocs += `${indent.get()}/// * [${helpers.wrapInBackTicks(header.name)}()](crate::generated::models::${traitName}::${header.name}) - ${header.header}\n`;
headerDocs += `${indent.get()}/// * [${helpers.wrapInBackTicks(header.name)}()](${utils.buildImportPath(module, module)}::models::${traitName}::${header.name}) - ${header.header}\n`;
}

headerDocs += `${indent.get()}///\n`;
headerDocs += `${indent.get()}/// [${helpers.wrapInBackTicks(traitName)}]: crate::generated::models::${traitName}\n`;
headerDocs += `${indent.get()}/// [${helpers.wrapInBackTicks(traitName)}]: ${utils.buildImportPath(module, module)}::models::${traitName}\n`;

return headerDocs;
}
Expand Down
154 changes: 121 additions & 33 deletions packages/typespec-rust/src/codegen/codeGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { emitUnions } from './unions.js';
import { Module } from './helpers.js';
import { emitLibRs } from './lib.js';
import { emitHeaderTraits } from './headerTraits.js';
import { emitClientsModRs, emitGeneratedModRs, emitModelsModRs } from './mod.js';
import { emitClientsModRs, emitGeneratedModRs, emitModelsModRs, emitSubModRs } from './mod.js';
import { emitModels } from './models.js';

import * as rust from '../codemodel/index.js';
Expand All @@ -37,6 +37,7 @@ export class CodeGenerator {
constructor(crate: rust.Crate) {
this.context = new Context(crate);
this.crate = crate;
sortContent(this.crate);
}

/**
Expand All @@ -54,7 +55,7 @@ export class CodeGenerator {
* @returns the content for lib.rs
*/
emitLibRs(): string {
return emitLibRs();
return emitLibRs(this.crate);
}

/**
Expand All @@ -63,54 +64,141 @@ export class CodeGenerator {
* @returns an array of files to emit
*/
emitContent(): Array<File> {
const modelsModRS = new Array<string>();
const files = new Array<File>();
const clientsSubDir = 'clients';
const modelsSubDir = 'models';
const generatedSubDir = 'generated';
const clientsSubDir = `${generatedSubDir}/clients`;
const modelsSubDir = `${generatedSubDir}/models`;

const addModelsFile = function (module: Module | undefined): void {
const addModelsFile = function (dir: string, files: Array<File>, modelsModRS: Array<string>, module: Module | undefined): void {
if (!module) {
return;
}
files.push({ name: `${modelsSubDir}/${module.name}.rs`, content: module.content });
files.push({ name: `${dir}${modelsSubDir}/${module.name}.rs`, content: module.content });
modelsModRS.push(`${module.visibility === 'pubCrate' ? 'pub(crate) ' : ''}mod ${module.name}`);
if (module.visibility !== 'internal') {
modelsModRS.push(`pub${module.visibility === 'pubCrate' ? '(crate)' : ''} use ${module.name}::*`);
}
};

const clientModules = emitClients(this.crate);
if (clientModules) {
files.push(...clientModules.modules.map((module) => { return { name: `${clientsSubDir}/${module.name}.rs`, content: module.content }; }));
files.push({ name: `${clientsSubDir}/mod.rs`, content: emitClientsModRs(clientModules.modules.map((module) => module.name)) });
addModelsFile(clientModules.options);
}
return this.recursiveEmit((module: rust.ModuleContainer, dir: string): Array<File> => {
const modelsModRS = new Array<string>();
const files = new Array<File>();

const clientModules = emitClients(module);
if (clientModules) {
files.push(...clientModules.modules.map((module) => { return { name: `${dir}${clientsSubDir}/${module.name}.rs`, content: module.content }; }));
files.push({ name: `${dir}${clientsSubDir}/mod.rs`, content: emitClientsModRs(clientModules.modules.map((module) => module.name)) });
addModelsFile(dir, files, modelsModRS, clientModules.options);
}

const enums = emitEnums(module, this.context);
addModelsFile(dir, files, modelsModRS, enums.definitions);
addModelsFile(dir, files, modelsModRS, enums.serde);
addModelsFile(dir, files, modelsModRS, enums.impls);

const unions = emitUnions(module, this.context);
addModelsFile(dir, files, modelsModRS, unions.definitions);
addModelsFile(dir, files, modelsModRS, unions.impls);
addModelsFile(dir, files, modelsModRS, unions.serde);

const models = emitModels(module, this.context);
addModelsFile(dir, files, modelsModRS, models.definitions);
addModelsFile(dir, files, modelsModRS, models.serde);
addModelsFile(dir, files, modelsModRS, models.impls);
addModelsFile(dir, files, modelsModRS, models.xmlHelpers);

addModelsFile(dir, files, modelsModRS, emitHeaderTraits(module));

if (modelsModRS.length > 0) {
files.push({ name: `${dir}${modelsSubDir}/mod.rs`, content: emitModelsModRs(modelsModRS) })
}

if (module.clients.length > 0 || module.enums.length > 0 || module.models.length > 0 || module.unions.length > 0) {
files.push({ name: `${dir}${generatedSubDir}/mod.rs`, content: emitGeneratedModRs(module) });
}

return files;
});
}

/**
* recursively emits module contents.
*
* @param emitForModule the module contents to emit
*/
private recursiveEmit(emitForModule: (module: rust.ModuleContainer, dir: string) => Array<File>): Array<File> {
const content = new Array<File>();
const recursiveEmit = (module: rust.ModuleContainer, dir: string): void => {
content.push(...emitForModule(module, dir));

// recursively emit any sub-modules
for (const subModule of module.subModules) {
const subModuleDir = `${dir}${subModule.name}/`;
content.push({
name: `${subModuleDir}mod.rs`,
content: emitSubModRs(subModule),
});
recursiveEmit(subModule, subModuleDir);
}
};

const enums = emitEnums(this.crate, this.context);
addModelsFile(enums.definitions);
addModelsFile(enums.serde);
addModelsFile(enums.impls);
recursiveEmit(this.crate, '');
return content
}
}

const unions = emitUnions(this.crate, this.context);
addModelsFile(unions.definitions);
addModelsFile(unions.impls);
addModelsFile(unions.serde);
/**
* recursively sorts code model contents by name in alphabetical order.
*
* @param content the contents to sort
*/
function sortContent(content: rust.ModuleContainer): void {
const sortAscending = function(a: string, b: string): number {
return a < b ? -1 : a > b ? 1 : 0;
};

if (content.kind === 'crate') {
content.dependencies.sort((a: rust.CrateDependency, b: rust.CrateDependency) => { return sortAscending(a.name, b.name); });
}

const models = emitModels(this.crate, this.context);
addModelsFile(models.definitions);
addModelsFile(models.serde);
addModelsFile(models.impls);
addModelsFile(models.xmlHelpers);
content.unions.sort((a: rust.DiscriminatedUnion, b: rust.DiscriminatedUnion) => { return sortAscending(a.name, b.name); });
for (const rustUnion of content.unions) {
rustUnion.members.sort((a: rust.DiscriminatedUnionMember, b: rust.DiscriminatedUnionMember) => { return sortAscending(a.type.name, b.type.name); });
}

addModelsFile(emitHeaderTraits(this.crate));
content.enums.sort((a: rust.Enum, b: rust.Enum) => { return sortAscending(a.name, b.name); });
for (const rustEnum of content.enums) {
rustEnum.values.sort((a: rust.EnumValue, b: rust.EnumValue) => { return sortAscending(a.name, b.name); });
}

content.models.sort((a: rust.MarkerType | rust.Model, b: rust.MarkerType | rust.Model) => { return sortAscending(a.name, b.name); });
for (const model of content.models) {
if (model.kind === 'marker') {
continue;
}
model.fields.sort((a: rust.ModelFieldType, b: rust.ModelFieldType) => { return sortAscending(a.name, b.name); });
}

if (modelsModRS.length > 0) {
files.push({ name: `${modelsSubDir}/mod.rs`, content: emitModelsModRs(modelsModRS) })
content.clients.sort((a: rust.Client, b: rust.Client) => { return sortAscending(a.name, b.name); });
for (const client of content.clients) {
client.fields.sort((a: rust.StructField, b: rust.StructField) => { return sortAscending(a.name, b.name); });
client.methods.sort((a: rust.MethodType, b: rust.MethodType) => { return sortAscending(a.name, b.name); });
if (client.constructable) {
client.constructable.options.type.fields.sort((a: rust.StructField, b: rust.StructField) => { return sortAscending(a.name, b.name); });
}
for (const method of client.methods) {
if (method.kind === 'clientaccessor') {
continue;
} else if (method.kind === 'pageable' && method.strategy?.kind === 'nextLink') {
method.strategy.reinjectedParams.sort((a: rust.MethodParameter, b: rust.MethodParameter) => sortAscending(a.name, b.name));
}
method.options.type.fields.sort((a: rust.StructField, b: rust.StructField) => { return sortAscending(a.name, b.name); });
method.responseHeaders?.headers.sort((a: rust.ResponseHeader, b: rust.ResponseHeader) => sortAscending(a.header, b.header));
}
}

// there will always be something in the generated/mod.rs file
files.push({ name: 'mod.rs', content: emitGeneratedModRs(this.crate) });
content.subModules.sort((a: rust.SubModule, b: rust.SubModule) => sortAscending(a.name, b.name));

return files;
for (const subModule of content.subModules) {
sortContent(subModule);
}
}
10 changes: 9 additions & 1 deletion packages/typespec-rust/src/codegen/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export class Context {
* @param crate the crate for which the context will be constructed
*/
constructor(crate: rust.Crate) {
this.recursivePopulate(crate);
}

private recursivePopulate(module: rust.ModuleContainer): void {
const recursiveAddBodyFormat = (type: rust.Type, format: helpers.ModelFormat) => {
type = helpers.unwrapType(type);
if (type.kind !== 'model') {
Expand All @@ -50,7 +54,7 @@ export class Context {

// enumerate all client methods, looking for enum and model
// params/responses and their wire format (JSON/XML etc).
for (const client of crate.clients) {
for (const client of module.clients) {
for (const method of client.methods) {
if (method.kind === 'clientaccessor') {
continue;
Expand Down Expand Up @@ -96,6 +100,10 @@ export class Context {
}
}
}

for (const subModule of module.subModules) {
this.recursivePopulate(subModule);
}
}

/**
Expand Down
Loading