diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..12b705634 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,29 @@ +root = true + +[*] +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +end_of_line = lf + +[*.{js,ts}] +indent_size = 2 + +[*.json] +indent_size = 2 + +[*.{yaml,yml}] +indent_size = 2 + +[*.md] +indent_size = 2 + +[*.tsp] +indent_size = 2 + +[*.toml] +indent_size = 2 + +[*.sh] +indent_size = 2 \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f9d9875b5..1bb528487 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -11,9 +11,20 @@ This document serves as an index to task-specific instructions for GitHub Copilo - Use `pnpm format` under each subfolder of `packages` folder to format all files. ## Coding Standards + - Validate all changes are linter clean by running `pnpm eslint`. - Make sure that cspell is installed and configured in your editor for spell checking. - Ensure that "cspell -c ./.vscode/cspell.json ./packages" succeeds before committing changes. +## Source Generation and Testing + +- Use `pnpm run tspcompile` to regenerate Rust code from TypeSpec files. +- Generated sources are located in `test/*/src/generated/` directories. +- Use `pnpm run test` to run TypeScript tests. +- Use `pnpm run test-ci` for CI-style testing with coverage and JUnit output. +- Use `pnpm run spector --serve` to run the spector server for TypeSpec testing. +- Use `cargo test` in generated Rust project directories to run Rust tests. + ## Files and Directories + - Content in files must end with a newline character. diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..a2a6b4098 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,19 @@ +# Build artifacts +dist/ +node_modules/ +*.log + +# Generated code +packages/typespec-rust/test/spector/*/src/generated/ + +# Lock files +pnpm-lock.yaml +package-lock.json +yarn.lock + +# TypeSpec files (no parser available for prettier) +*.tsp + +# Temporary files +*.tmp +*.temp \ No newline at end of file diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 8b5ddc4a3..f546306b7 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -1,53 +1,53 @@ { - "version": "0.2", - "language": "en", - "useGitignore": true, - "ignorePaths": [ - "**/test-resources.bicep", - "**/test-resources.json", - "**/test-resources-post.ps1", - "**/assets.json", - ".config", - "packages/*/test/spector/**/*.rs", - "packages/*/test/sdk/**/*.rs", - "packages/*/test/spector/**/*.toml", - "packages/*/test/**/*.tsp", - "packages/*/pnpm-lock.yaml", - "**/package.json", - ".devcontainer/devcontainer.json", - ".devcontainer/Dockerfile", - ".devcontainer/oncreate", - ".github/CODEOWNERS", - ".github/dependabot.yml", - ".vscode/cspell.json", - ".vscode/extensions.json", - ".vscode/settings.json", - ".vscode/tasks.json", - "NOTICE.txt", - "SUPPORT.md", - "deny.toml", - "eng/", - "**/.dict.txt", - "rust-toolchain.toml" - ], - "words": [ - "appconfiguration", - "clientaccessor", - "clippy", - "codegen", - "codemodel", - "entra", - "enumvalue", - "impls", - "linq", - "msrc", - "pageable", - "reqwest", - "safeint", - "serde", - "spector", - "tcgc", - "tspcompile", - "typespec", - ] -} \ No newline at end of file + "version": "0.2", + "language": "en", + "useGitignore": true, + "ignorePaths": [ + "**/test-resources.bicep", + "**/test-resources.json", + "**/test-resources-post.ps1", + "**/assets.json", + ".config", + "packages/*/test/spector/**/*.rs", + "packages/*/test/sdk/**/*.rs", + "packages/*/test/spector/**/*.toml", + "packages/*/test/**/*.tsp", + "packages/*/pnpm-lock.yaml", + "**/package.json", + ".devcontainer/devcontainer.json", + ".devcontainer/Dockerfile", + ".devcontainer/oncreate", + ".github/CODEOWNERS", + ".github/dependabot.yml", + ".vscode/cspell.json", + ".vscode/extensions.json", + ".vscode/settings.json", + ".vscode/tasks.json", + "NOTICE.txt", + "SUPPORT.md", + "deny.toml", + "eng/", + "**/.dict.txt", + "rust-toolchain.toml" + ], + "words": [ + "appconfiguration", + "clientaccessor", + "clippy", + "codegen", + "codemodel", + "entra", + "enumvalue", + "impls", + "linq", + "msrc", + "pageable", + "reqwest", + "safeint", + "serde", + "spector", + "tcgc", + "tspcompile", + "typespec" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index c43a29ca0..4aab06fbf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,15 +1,12 @@ { - "cSpell.enabled": true, - "editor.formatOnSave": true, - "markdownlint.config": { - "MD024": false, - }, - "markdownlint.ignore": [ - "**/SECURITY.md", - "LICENSE" - ], - "yaml.format.printWidth": 240, - "[powershell]": { - "editor.defaultFormatter": "ms-vscode.powershell", - }, + "cSpell.enabled": true, + "editor.formatOnSave": true, + "markdownlint.config": { + "MD024": false + }, + "markdownlint.ignore": ["**/SECURITY.md", "LICENSE"], + "yaml.format.printWidth": 240, + "[powershell]": { + "editor.defaultFormatter": "ms-vscode.powershell" + } } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 316cc70af..bd2cf1b48 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,5 @@ - + # Contributing Guide This guide explains how to contribute to the TypeSpec Rust emitter project, including how to build, test, and develop the codebase. @@ -7,7 +7,7 @@ This guide explains how to contribute to the TypeSpec Rust emitter project, incl ## Prerequisites - **Node.js** (>=20.0.0) - Required for the TypeScript emitter -- **pnpm** (10.10.0) - Package manager for Node.js dependencies +- **pnpm** (10.10.0) - Package manager for Node.js dependencies - **Rust** (1.80+) - Required for building and testing generated Rust code - **Git** - Version control @@ -154,7 +154,7 @@ cargo test --no-fail-fast Stop the spector server when done: ```bash -cd packages/typespec-rust +cd packages/typespec-rust pnpm spector --stop ``` @@ -198,7 +198,7 @@ cd packages/typespec-rust pnpm eslint ``` -#### Linting Rust Code +#### Linting Rust Code ```bash cd packages/typespec-rust/test @@ -254,8 +254,8 @@ cargo fmt --all # TypeScript tests cd packages/typespec-rust pnpm test - - # Rust integration tests + + # Rust integration tests pnpm spector --start cd test/spector cargo test @@ -269,7 +269,7 @@ cargo fmt --all # TypeScript linting cd packages/typespec-rust pnpm eslint - + # Rust linting cd test cargo clippy --workspace --all-features --all-targets @@ -315,7 +315,7 @@ Update the version in `packages/typespec-rust/package.json` and document changes The CI pipeline runs the following checks on every pull request: 1. **Build** - Compiles TypeScript emitter -2. **Lint** - Runs ESLint on TypeScript code +2. **Lint** - Runs ESLint on TypeScript code 3. **Test** - Runs TypeScript unit tests with coverage 4. **Regenerate** - Regenerates all test crates and verifies no changes 5. **Compile** - Builds all generated Rust crates diff --git a/README.md b/README.md index 20c769a69..457258eaf 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,68 @@ # TypeSpec Rust Emitter +## Development + +### Prerequisites + +- Node.js 20 or later +- pnpm 10.x +- Rust toolchain (managed via `rust-toolchain.toml`) + +### Setup + +```bash +cd packages/typespec-rust +pnpm install +``` + +### Code Formatting + +This project enforces code formatting for all source files. The CI pipeline will check that all code follows the defined formatting rules. + +#### Supported File Types + +- **TypeScript/JavaScript**: Formatted using ESLint with automatic fixes +- **Markdown/JSON**: Formatted using Prettier +- **Rust**: Formatted using rustfmt +- **TypeSpec**: Currently validated for syntax (formatting tools not yet available) + +#### Local Development Scripts + +From the repository root: + +```bash +# Check formatting for all file types +./scripts/check-all-formats.sh + +# Format all files automatically +./scripts/format-all.sh + +# Check individual file types +./scripts/check-rust-format.sh +./scripts/check-typespec-format.sh +``` + +From the `packages/typespec-rust` directory: + +```bash +# Format TypeScript and run ESLint +pnpm run format + +# Check formatting without making changes +pnpm run format:check + +# Format specific file types +pnpm run format:prettier # Markdown, JSON, TypeScript +pnpm run format:eslint # TypeScript/JavaScript with ESLint +``` + +#### CI Pipeline + +The GitHub Actions workflow automatically checks formatting on all pull requests and pushes to main/master branches. All checks must pass before code can be merged. + ## Contributing -This project welcomes contributions and suggestions. Most contributions require you to agree to a +This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit . diff --git a/SECURITY.md b/SECURITY.md index b3c89efc8..28b2488ac 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,19 +12,19 @@ If you believe you have found a security vulnerability in any Microsoft-owned re Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). -If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). -You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: - * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) - * Full paths of source file(s) related to the manifestation of the issue - * The location of the affected source code (tag/branch/commit or direct URL) - * Any special configuration required to reproduce the issue - * Step-by-step instructions to reproduce the issue - * Proof-of-concept or exploit code (if possible) - * Impact of the issue, including how an attacker might exploit the issue +- Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) +- Full paths of source file(s) related to the manifestation of the issue +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- Step-by-step instructions to reproduce the issue +- Proof-of-concept or exploit code (if possible) +- Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. diff --git a/SUPPORT.md b/SUPPORT.md index 291d4d437..6892160ad 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -6,20 +6,20 @@ - **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps. - **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide. -*Then remove this first heading from this SUPPORT.MD file before publishing your repo.* +_Then remove this first heading from this SUPPORT.MD file before publishing your repo._ # Support -## How to file issues and get help +## How to file issues and get help -This project uses GitHub Issues to track bugs and feature requests. Please search the existing -issues before filing new issues to avoid duplicates. For new issues, file your bug or +This project uses GitHub Issues to track bugs and feature requests. Please search the existing +issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new Issue. -For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE +For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER CHANNEL. WHERE WILL YOU HELP PEOPLE?**. -## Microsoft Support Policy +## Microsoft Support Policy Support for this **PROJECT or PRODUCT** is limited to the resources listed above. diff --git a/eng/pipelines/templates/steps/build-test.yaml b/eng/pipelines/templates/steps/build-test.yaml index 5abcbf443..d49dd3f3b 100644 --- a/eng/pipelines/templates/steps/build-test.yaml +++ b/eng/pipelines/templates/steps/build-test.yaml @@ -10,6 +10,28 @@ steps: displayName: Run Lint workingDirectory: $(TypeSpecRustPkgDir) + - script: | + pnpm run format:check + displayName: Check Code Formatting + workingDirectory: $(TypeSpecRustPkgDir) + + - script: | + prettier --check "**/*.{md,json,ts,js}" \ + --ignore-path .gitignore \ + --ignore-path .prettierignore + displayName: Check Prettier Formatting + workingDirectory: $(System.DefaultWorkingDirectory) + + - script: | + ./scripts/check-typespec-format.sh + displayName: Validate TypeSpec Files + workingDirectory: $(System.DefaultWorkingDirectory) + + - script: | + ./scripts/check-rust-format.sh + displayName: Check Rust Formatting + workingDirectory: $(System.DefaultWorkingDirectory) + - script: | cspell -c .vscode/cspell.json . displayName: Run Spell Check diff --git a/eng/pipelines/templates/steps/set-env.yaml b/eng/pipelines/templates/steps/set-env.yaml index 4fe345277..def99edb5 100644 --- a/eng/pipelines/templates/steps/set-env.yaml +++ b/eng/pipelines/templates/steps/set-env.yaml @@ -17,6 +17,9 @@ steps: - script: npm install -g cspell displayName: Install cspell + - script: npm install -g prettier + displayName: Install prettier + - script: | npm i -g corepack@latest corepack enable diff --git a/packages/typespec-rust/.scripts/semaphore.js b/packages/typespec-rust/.scripts/semaphore.js index af72783e0..ebcde5835 100644 --- a/packages/typespec-rust/.scripts/semaphore.js +++ b/packages/typespec-rust/.scripts/semaphore.js @@ -2,7 +2,9 @@ // original sources can be found at https://github.com/abrkn/semaphore.js 'use strict'; -var nextTick = function (fn) { setTimeout(fn, 0); } +var nextTick = function (fn) { + setTimeout(fn, 0); +}; if (typeof process != 'undefined' && process && typeof process.nextTick == 'function') { // node.js and the like nextTick = process.nextTick; @@ -15,7 +17,7 @@ export function semaphore(capacity) { queue: [], firstHere: false, - take: function() { + take: function () { if (semaphore.firstHere === false) { semaphore.current++; semaphore.firstHere = true; @@ -31,13 +33,15 @@ export function semaphore(capacity) { item.n = arguments[0]; } - if (arguments.length >= 2) { + if (arguments.length >= 2) { if (typeof arguments[1] == 'function') item.task = arguments[1]; else item.n = arguments[1]; } var task = item.task; - item.task = function() { task(semaphore.leave); }; + item.task = function () { + task(semaphore.leave); + }; if (semaphore.current + item.n - isFirst > semaphore.capacity) { if (isFirst === 1) { @@ -52,7 +56,7 @@ export function semaphore(capacity) { if (isFirst === 1) semaphore.firstHere = false; }, - leave: function(n) { + leave: function (n) { n = n || 1; semaphore.current -= n; @@ -77,14 +81,14 @@ export function semaphore(capacity) { nextTick(item.task); }, - available: function(n) { + available: function (n) { n = n || 1; - return(semaphore.current + n <= semaphore.capacity); - } + return semaphore.current + n <= semaphore.capacity; + }, }; return semaphore; -}; +} if (typeof exports === 'object') { // node export @@ -92,7 +96,7 @@ if (typeof exports === 'object') { } else if (typeof define === 'function' && define.amd) { // amd export define(function () { - return semaphore; + return semaphore; }); } else { // browser global diff --git a/packages/typespec-rust/.scripts/spector.js b/packages/typespec-rust/.scripts/spector.js index 800741727..11bd37c1d 100644 --- a/packages/typespec-rust/.scripts/spector.js +++ b/packages/typespec-rust/.scripts/spector.js @@ -14,7 +14,7 @@ switch (process.argv[2]) { switches.push('serve'); switches.push(httpSpecs); switches.push(azureHttpSpecs); - execSyncOptions = {stdio: 'inherit'}; + execSyncOptions = { stdio: 'inherit' }; break; case '--start': switches.push('server'); diff --git a/packages/typespec-rust/.scripts/tspcompile.js b/packages/typespec-rust/.scripts/tspcompile.js index 4b2cc5942..b8b3c23d7 100644 --- a/packages/typespec-rust/.scripts/tspcompile.js +++ b/packages/typespec-rust/.scripts/tspcompile.js @@ -21,51 +21,51 @@ const compiler = pkgRoot + 'node_modules/@typespec/compiler/cmd/tsp.js'; const httpSpecsGroup = { //'spector_apikey': {input: 'authentication/api-key'}, //'spector_custom': {input: 'authentication/http/custom'}, - 'spector_oauth2': {input: 'authentication/oauth2'}, - 'spector_unionauth': {input: 'authentication/union'}, - 'spector_bytes': {input: 'encode/bytes'}, // TODO: nested arrays and "raw" request/responses (i.e. the orphan problem) - 'spector_datetime': {input: 'encode/datetime'}, - 'spector_duration': {input: 'encode/duration'}, - 'spector_numeric': {input: 'encode/numeric'}, - 'spector_bodyoptional': {input: 'parameters/body-optionality'}, - 'spector_basicparams': {input: 'parameters/basic'}, - 'spector_collectionfmt': {input: 'parameters/collection-format'}, - 'spector_spread': {input: 'parameters/spread'}, - 'spector_contentneg': {input: 'payload/content-negotiation'}, - 'spector_jmergepatch': {input: 'payload/json-merge-patch'}, + spector_oauth2: { input: 'authentication/oauth2' }, + spector_unionauth: { input: 'authentication/union' }, + spector_bytes: { input: 'encode/bytes' }, // TODO: nested arrays and "raw" request/responses (i.e. the orphan problem) + spector_datetime: { input: 'encode/datetime' }, + spector_duration: { input: 'encode/duration' }, + spector_numeric: { input: 'encode/numeric' }, + spector_bodyoptional: { input: 'parameters/body-optionality' }, + spector_basicparams: { input: 'parameters/basic' }, + spector_collectionfmt: { input: 'parameters/collection-format' }, + spector_spread: { input: 'parameters/spread' }, + spector_contentneg: { input: 'payload/content-negotiation' }, + spector_jmergepatch: { input: 'payload/json-merge-patch' }, //'spector_corepageable': {input: 'payload/pageable'}, // TODO: https://github.com/Azure/typespec-rust/issues/508 //'spector_mediatype': {input: 'payload/media-type'}, //'spector_multipart': {input: 'payload/multipart'}, - 'spector_xml': {input: 'payload/xml'}, - 'spector_routes': {input: 'routes'}, - 'spector_jsonencodedname': {input: 'serialization/encoded-name/json'}, - 'spector_noendpoint': {input: 'server/endpoint/not-defined'}, - 'spector_multiple': {input: 'server/path/multiple'}, - 'spector_single': {input: 'server/path/single'}, - 'spector_unversioned': {input: 'server/versions/not-versioned'}, - 'spector_versioned': {input: 'server/versions/versioned'}, + spector_xml: { input: 'payload/xml' }, + spector_routes: { input: 'routes' }, + spector_jsonencodedname: { input: 'serialization/encoded-name/json' }, + spector_noendpoint: { input: 'server/endpoint/not-defined' }, + spector_multiple: { input: 'server/path/multiple' }, + spector_single: { input: 'server/path/single' }, + spector_unversioned: { input: 'server/versions/not-versioned' }, + spector_versioned: { input: 'server/versions/versioned' }, //'spector_condreq': {input: 'special-headers/conditional-request'}, //'spector_repeatability': {input: 'special-headers/repeatability'}, - 'spector_specialwords': {input: 'special-words'}, - 'spector_array': {input: 'type/array'}, // needs additional codegen work before we can add tests - 'spector_dictionary': {input: 'type/dictionary'}, // needs additional codegen work before we can add tests - 'spector_extensible': {input: 'type/enum/extensible'}, - 'spector_fixed': {input: 'type/enum/fixed'}, - 'spector_empty': {input: 'type/model/empty'}, + spector_specialwords: { input: 'special-words' }, + spector_array: { input: 'type/array' }, // needs additional codegen work before we can add tests + spector_dictionary: { input: 'type/dictionary' }, // needs additional codegen work before we can add tests + spector_extensible: { input: 'type/enum/extensible' }, + spector_fixed: { input: 'type/enum/fixed' }, + spector_empty: { input: 'type/model/empty' }, //'spector_enumdisc': {input: 'type/model/inheritance/enum-discriminator'}, - 'spector_nodisc': {input: 'type/model/inheritance/not-discriminated'}, + spector_nodisc: { input: 'type/model/inheritance/not-discriminated' }, //'spector_recursive': {input: 'type/model/inheritance/recursive'}, //'spector_singledisc': {input: 'type/model/inheritance/single-discriminator'}, - 'spector_usage': {input: 'type/model/usage'}, + spector_usage: { input: 'type/model/usage' }, //'spector_visibility': {input: 'type/model/visibility'}, //'spector_addlprops': {input: 'type/property/additional-properties'}, - 'spector_nullable': {input: 'type/property/nullable'}, - 'spector_optionality': {input: 'type/property/optionality'}, - 'spector_valuetypes': {input: 'type/property/value-types'}, - 'spector_scalar': {input: 'type/scalar'}, + spector_nullable: { input: 'type/property/nullable' }, + spector_optionality: { input: 'type/property/optionality' }, + spector_valuetypes: { input: 'type/property/value-types' }, + spector_scalar: { input: 'type/scalar' }, //'spector_union': {input: 'type/union'}, //'spector_veradded': {input: 'versioning/added'}, - 'spector_madeoptional': {input: 'versioning/madeOptional'}, + spector_madeoptional: { input: 'versioning/madeOptional' }, //'spector_verremoved': {input: 'versioning/removed'}, //'spector_renamedfrom': {input: 'versioning/renamedFrom'}, //'spector_returntypechanged': {input: 'versioning/returnTypeChangedFrom'}, @@ -74,40 +74,40 @@ const httpSpecsGroup = { const azureHttpSpecsGroup = { //'spector_access': {input: 'azure/client-generator-core/access'}, - 'spector_apiverheader': {input: 'azure/client-generator-core/api-version/header/client.tsp'}, - 'spector_apiverpath': {input: 'azure/client-generator-core/api-version/path/client.tsp'}, - 'spector_apiverquery': {input: 'azure/client-generator-core/api-version/query/client.tsp'}, - 'spector_clientinit': {input: 'azure/client-generator-core/client-initialization/client.tsp'}, - 'spector_flattenproperty': {input: 'azure/client-generator-core/flatten-property'}, - 'spector_coreusage': {input: 'azure/client-generator-core/usage'}, - 'spector_basic': {input: 'azure/core/basic'}, + spector_apiverheader: { input: 'azure/client-generator-core/api-version/header/client.tsp' }, + spector_apiverpath: { input: 'azure/client-generator-core/api-version/path/client.tsp' }, + spector_apiverquery: { input: 'azure/client-generator-core/api-version/query/client.tsp' }, + spector_clientinit: { input: 'azure/client-generator-core/client-initialization/client.tsp' }, + spector_flattenproperty: { input: 'azure/client-generator-core/flatten-property' }, + spector_coreusage: { input: 'azure/client-generator-core/usage' }, + spector_basic: { input: 'azure/core/basic' }, //'spector_lrorpc': {input: 'azure/core/lro/rpc'}, - 'spector_lrostd': {input: 'azure/core/lro/standard'}, + spector_lrostd: { input: 'azure/core/lro/standard' }, //'spector_coremodel': {input: 'azure/core/model'}, - 'spector_corepage': {input: 'azure/core/page'}, + spector_corepage: { input: 'azure/core/page' }, //'spector_corescalar': {input: 'azure/core/scalar'}, //'spector_traits': {input: 'azure/core/traits'}, - 'spector_azureduration': {input: 'azure/encode/duration'}, - 'spector_azurepageable': {input: 'azure/payload/pageable'}, - 'spector_azurebasic': {input: 'azure/example/basic'}, - 'spector_armcommon': {input: 'azure/resource-manager/common-properties'}, - 'spector_armnonresource': {input: 'azure/resource-manager/non-resource'}, - 'spector_armoptemplates': {input: 'azure/resource-manager/operation-templates'}, - 'spector_armresources': {input: 'azure/resource-manager/resources'}, - 'spector_naming': {input: 'client/naming'}, - 'spector_clientopgroup': {input: 'client/structure/client-operation-group/client.tsp'}, - 'spector_default': {input: 'client/structure/default/client.tsp'}, - 'spector_multiclient': {input: 'client/structure/multi-client/client.tsp'}, - 'spector_renamedop': {input: 'client/structure/renamed-operation/client.tsp'}, - 'spector_twoop': {input: 'client/structure/two-operation-group/client.tsp'}, - 'spector_srvdrivenold': {input: 'resiliency/srv-driven/old.tsp', output: 'resiliency/srv-driven/old'}, - 'spector_srvdrivennew': {input: 'resiliency/srv-driven', output: 'resiliency/srv-driven/new'}, + spector_azureduration: { input: 'azure/encode/duration' }, + spector_azurepageable: { input: 'azure/payload/pageable' }, + spector_azurebasic: { input: 'azure/example/basic' }, + spector_armcommon: { input: 'azure/resource-manager/common-properties' }, + spector_armnonresource: { input: 'azure/resource-manager/non-resource' }, + spector_armoptemplates: { input: 'azure/resource-manager/operation-templates' }, + spector_armresources: { input: 'azure/resource-manager/resources' }, + spector_naming: { input: 'client/naming' }, + spector_clientopgroup: { input: 'client/structure/client-operation-group/client.tsp' }, + spector_default: { input: 'client/structure/default/client.tsp' }, + spector_multiclient: { input: 'client/structure/multi-client/client.tsp' }, + spector_renamedop: { input: 'client/structure/renamed-operation/client.tsp' }, + spector_twoop: { input: 'client/structure/two-operation-group/client.tsp' }, + spector_srvdrivenold: { input: 'resiliency/srv-driven/old.tsp', output: 'resiliency/srv-driven/old' }, + spector_srvdrivennew: { input: 'resiliency/srv-driven', output: 'resiliency/srv-driven/new' }, }; const args = process.argv.slice(2); var filter = undefined; const switches = []; -for (var i = 0 ; i < args.length; i += 1) { +for (var i = 0; i < args.length; i += 1) { const filterArg = args[i].match(/--filter=(?\w+)/); if (filterArg) { filter = filterArg.groups['filter']; @@ -123,15 +123,15 @@ for (var i = 0 ; i < args.length; i += 1) { } if (filter !== undefined) { - console.log("Using filter: " + filter) + console.log('Using filter: ' + filter); } function should_generate(name) { if (filter !== undefined) { const re = new RegExp(filter); - return re.test(name) + return re.test(name); } - return true + return true; } const appconfiguration = pkgRoot + 'test/tsp/AppConfiguration'; @@ -146,8 +146,8 @@ generate('blob_storage', blob_storage, 'test/sdk/blob_storage', ['temp-omit-doc- const serde_tests = pkgRoot + 'test/tsp/SerdeTests'; generate('serde_tests', serde_tests, 'test/other/serde_tests'); -loopSpec(httpSpecsGroup, httpSpecs) -loopSpec(azureHttpSpecsGroup, azureHttpSpecs) +loopSpec(httpSpecsGroup, httpSpecs); +loopSpec(azureHttpSpecsGroup, azureHttpSpecs); function loopSpec(group, root) { for (const crate in group) { @@ -173,7 +173,7 @@ function loopSpec(group, root) { function generate(crate, input, outputDir, additionalArgs) { if (!should_generate(crate)) { - return + return; } if (additionalArgs === undefined) { additionalArgs = []; @@ -182,7 +182,7 @@ function generate(crate, input, outputDir, additionalArgs) { additionalArgs[i] = `--option="@azure-tools/typespec-rust.${additionalArgs[i]}"`; } } - sem.take(function() { + sem.take(function () { // default to main.tsp if a .tsp file isn't specified in the input if (input.lastIndexOf('.tsp') === -1) { input += '/main.tsp'; @@ -201,7 +201,7 @@ function generate(crate, input, outputDir, additionalArgs) { // delete all content before regenerating as it makes it // really easy to determine if something failed to generated fs.rmSync(path.join(fullOutputDir, 'src', 'generated'), { force: true, recursive: true }); - exec(command, function(error, stdout, stderr) { + exec(command, function (error, stdout, stderr) { // print any output or error from the tsp compile command logResult(error, stdout, stderr); }); diff --git a/packages/typespec-rust/.scripts/workspace-members.js b/packages/typespec-rust/.scripts/workspace-members.js index 2f55670d3..520536647 100644 --- a/packages/typespec-rust/.scripts/workspace-members.js +++ b/packages/typespec-rust/.scripts/workspace-members.js @@ -9,7 +9,7 @@ const workspaceRoot = execSync('git rev-parse --show-toplevel').toString().trim( const entries = fs.readdirSync(workspaceRoot, { recursive: true, withFileTypes: true }); entries.forEach((entry) => { - if (entry.isFile() && entry.name === 'Cargo.toml' && entry.parentPath !== workspaceRoot) { - console.log(` "${entry.parentPath.substring(workspaceRoot.length + 1).replaceAll('\\', '/')}",`); - } + if (entry.isFile() && entry.name === 'Cargo.toml' && entry.parentPath !== workspaceRoot) { + console.log(` "${entry.parentPath.substring(workspaceRoot.length + 1).replaceAll('\\', '/')}",`); + } }); diff --git a/packages/typespec-rust/CHANGELOG.md b/packages/typespec-rust/CHANGELOG.md index 52c5ee2fc..620cac41a 100644 --- a/packages/typespec-rust/CHANGELOG.md +++ b/packages/typespec-rust/CHANGELOG.md @@ -6,12 +6,12 @@ **NOTE: this version is incompatible with earlier versions of `azure_core`** -* Renamed `PagerResult::More { next, .. }` to `PagerResult::More { continuation, .. }`. +- Renamed `PagerResult::More { next, .. }` to `PagerResult::More { continuation, .. }`. ### Other Changes -* Fixed malformed doc comments for enums and their values. -* Retooled some usage of `format!` macros. +- Fixed malformed doc comments for enums and their values. +- Retooled some usage of `format!` macros. ## 0.17.0 (2025-06-19) @@ -19,11 +19,11 @@ **NOTE: this version is incompatible with earlier versions of `azure_core`** -* Switch to using `OffsetDateTime` from `azure_core::time` instead of the `time` crate. +- Switch to using `OffsetDateTime` from `azure_core::time` instead of the `time` crate. ### Features Added -* Added support for stylized path collection parameters. +- Added support for stylized path collection parameters. ## 0.16.0 (2025-06-12) @@ -31,13 +31,13 @@ **NOTE: this version is incompatible with earlier versions of `azure_core`** -* Methods that return a raw response with a marker type or no response have a return type of `Response`. +- Methods that return a raw response with a marker type or no response have a return type of `Response`. ### Other Changes -* Small refactoring to pageable method bodies. -* Updated to the latest tsp toolset. -* Response headers decorated with `Access.internal` are omitted from the response headers trait. +- Small refactoring to pageable method bodies. +- Updated to the latest tsp toolset. +- Response headers decorated with `Access.internal` are omitted from the response headers trait. ## 0.15.0 (2025-06-05) @@ -45,98 +45,98 @@ **NOTE: this version is incompatible with earlier versions of `azure_core`** -* Updated method bodies to use the new `azure_core::http::Format` in the pipeline. -* Methods that return a streaming response now return a `Result` type. -* Updated implementations of paged methods per changes in `azure_core`. - * Paged operations that return a collection and "next link" now return a per-item iterator of type `Pager`. - * If a paged operation returns more than the above, it returns a `PageIterator` which behaves like previous versions of `Pager`. +- Updated method bodies to use the new `azure_core::http::Format` in the pipeline. +- Methods that return a streaming response now return a `Result` type. +- Updated implementations of paged methods per changes in `azure_core`. + - Paged operations that return a collection and "next link" now return a per-item iterator of type `Pager`. + - If a paged operation returns more than the above, it returns a `PageIterator` which behaves like previous versions of `Pager`. ### Bugs Fixed -* Fixed handling for required API version client parameter. -* Don't propagate parent client fields to child clients that aren't used by the child. -* Fixed incorrect `content-type` header parameter when the request body is optional. -* Fixed some rare cases where a field name could start with an underscore character. +- Fixed handling for required API version client parameter. +- Don't propagate parent client fields to child clients that aren't used by the child. +- Fixed incorrect `content-type` header parameter when the request body is optional. +- Fixed some rare cases where a field name could start with an underscore character. ## 0.14.2 (2025-05-27) ### Features Added -* Added support for `plainDate` and `plainTime` types. They're emitted as `String` types. -* Added support for the `safeint` type. It's emitted as a `serde_json::Number` type. +- Added support for `plainDate` and `plainTime` types. They're emitted as `String` types. +- Added support for the `safeint` type. It's emitted as a `serde_json::Number` type. ### Bugs Fixed -* Fixed bad codegen when path parameters are aliased as client initializers. -* Fixed bad codegen for model with literal values. -* Fixed incorrect behavior for numeric types that use string encoding. -* Fixed decimal types to properly handle string/float encodings. +- Fixed bad codegen when path parameters are aliased as client initializers. +- Fixed bad codegen for model with literal values. +- Fixed incorrect behavior for numeric types that use string encoding. +- Fixed decimal types to properly handle string/float encodings. ## 0.14.1 (2025-05-07) ### Bugs Fixed -* Fixed infinite loop for certain paged operations. -* Fixed missing borrow for required header parameters that are used in a closure (e.g. pageable operations). -* Fixed missing header constant when header traits are merged. -* Don't skip core types when they're explicitly referenced. -* Fixed missing content type for operations that have multiple responses and one of them doesn't include a response body (e.g. 200 and 204). -* Fixed more cases of enum names with symbols that can't be in an identifier. +- Fixed infinite loop for certain paged operations. +- Fixed missing borrow for required header parameters that are used in a closure (e.g. pageable operations). +- Fixed missing header constant when header traits are merged. +- Don't skip core types when they're explicitly referenced. +- Fixed missing content type for operations that have multiple responses and one of them doesn't include a response body (e.g. 200 and 204). +- Fixed more cases of enum names with symbols that can't be in an identifier. ### Other Changes -* Updated to the latest tsp toolset. - * This includes the GA version of the compiler and supporting libraries. +- Updated to the latest tsp toolset. + - This includes the GA version of the compiler and supporting libraries. ## 0.14.0 (2025-05-01) ### Breaking Changes -* Model fields of type `HashMap` and `Vec` are now wrapped in an `Option`. - * The only exception is for the `Vec` in paged responses. -* Parameters emitted as `&str` but required ownership are now emitted as `String`. -* Parameters of type `Vec` that don't require ownership are now `&[T]`. +- Model fields of type `HashMap` and `Vec` are now wrapped in an `Option`. + - The only exception is for the `Vec` in paged responses. +- Parameters emitted as `&str` but required ownership are now emitted as `String`. +- Parameters of type `Vec` that don't require ownership are now `&[T]`. ### Features Added -* Added support for pageable methods that use a continuation token when fetching pages. -* Added support for TypeSpec `decimal` and `decimal128` types. +- Added support for pageable methods that use a continuation token when fetching pages. +- Added support for TypeSpec `decimal` and `decimal128` types. ### Bugs Fixed -* Fixed XML helpers for certain cases of wrapped arrays. -* Avoid infinite recursion in emitted types by using `Box` to break the cycle. +- Fixed XML helpers for certain cases of wrapped arrays. +- Avoid infinite recursion in emitted types by using `Box` to break the cycle. ### Other Changes -* Errors in the emitter are no longer surfaced as a crash. -* Skip `cargo fmt` if the emitter fails. +- Errors in the emitter are no longer surfaced as a crash. +- Skip `cargo fmt` if the emitter fails. ## 0.13.3 (2025-04-04) ### Other Changes -* Nullable types are treated as their underlying type (temporary until `Nullable` arrives in core). +- Nullable types are treated as their underlying type (temporary until `Nullable` arrives in core). ## 0.13.2 (2025-04-03) ### Other Changes -* Add doc comments for fields in client options types. -* Add missing doc comment(s) for multiple response header traits that get merged into a single trait. -* Added switch `temp-omit-doc-links` to omit links to types in doc comments. - * NOTE: this switch is _temporary_ and will be removed in a future release. -* Updated to the latest tsp toolset. - * This prompted updating the minimum node engine to `v20.x.y`. +- Add doc comments for fields in client options types. +- Add missing doc comment(s) for multiple response header traits that get merged into a single trait. +- Added switch `temp-omit-doc-links` to omit links to types in doc comments. + - NOTE: this switch is _temporary_ and will be removed in a future release. +- Updated to the latest tsp toolset. + - This prompted updating the minimum node engine to `v20.x.y`. ## 0.13.1 (2025-04-01) ### Other Changes -* Recursively delete the contents of `src/generated` before writing the content to disk. -* Consolidate `use` statements. -* Omit `DO NOT EDIT` phrase from `src/lib.rs`. -* Skip LRO methods instead of erroring out. +- Recursively delete the contents of `src/generated` before writing the content to disk. +- Consolidate `use` statements. +- Omit `DO NOT EDIT` phrase from `src/lib.rs`. +- Skip LRO methods instead of erroring out. ## 0.13.0 (2025-03-24) @@ -144,269 +144,269 @@ **NOTE: this version is incompatible with earlier versions of `azure_core`** -* Updated references to types in `azure_core` based on its refactoring. -* Replaced references to `typespec_client_core` with the matching references in `azure_core`. +- Updated references to types in `azure_core` based on its refactoring. +- Replaced references to `typespec_client_core` with the matching references in `azure_core`. ### Other Changes -* Use `crate::generated::` paths to types in doc links. +- Use `crate::generated::` paths to types in doc links. ## 0.12.0 (2025-03-20) ### Breaking Changes -* The word `Etag` is no longer snake-cased to `e_tag`. +- The word `Etag` is no longer snake-cased to `e_tag`. ### Bugs Fixed -* Fixed serde for models containing hash maps/vectors of base64 encoded bytes and hash maps/vectors of `OffsetDateTime` types. -* Fixed an issue that could cause emitted code to use incorrect base64 encoding/decoding. -* Fixed serde annotations to omit empty `Vec` for XML unwrapped arrays. -* Remove erroneous `url = url.join("")?;` that can happen in some cases. +- Fixed serde for models containing hash maps/vectors of base64 encoded bytes and hash maps/vectors of `OffsetDateTime` types. +- Fixed an issue that could cause emitted code to use incorrect base64 encoding/decoding. +- Fixed serde annotations to omit empty `Vec` for XML unwrapped arrays. +- Remove erroneous `url = url.join("")?;` that can happen in some cases. ### Other Changes -* Updated to the latest tsp toolset. -* Client struct fields are now always `pub(crate)`. In addition, internal and helper types are now `pub(crate)` instead of `pub` to help prevent inadvertent exposure. -* Report diagnostics from `@azure-tools/typespec-client-generator-core`. -* Refactor on-disk layout of generated code (simplifies re-exporting of types). -* The `lib.rs` file is no longer merged and will be ignored when it exists (a warning diagnostic is displayed). - * Set `overwrite-lib-rs: true` to force overwriting the `lib.rs` file. +- Updated to the latest tsp toolset. +- Client struct fields are now always `pub(crate)`. In addition, internal and helper types are now `pub(crate)` instead of `pub` to help prevent inadvertent exposure. +- Report diagnostics from `@azure-tools/typespec-client-generator-core`. +- Refactor on-disk layout of generated code (simplifies re-exporting of types). +- The `lib.rs` file is no longer merged and will be ignored when it exists (a warning diagnostic is displayed). + - Set `overwrite-lib-rs: true` to force overwriting the `lib.rs` file. ## 0.11.0 (2025-03-04) ### Breaking Changes -* Pageable methods will be renamed to start with `list` (e.g. `get_versions` becomes `list_versions`). A warning diagnostic is displayed when such a rename occurs. -* Sub-clients that specify a `@clientName` decorator will use that client name verbatim instead of having the parent client name as a prefix. +- Pageable methods will be renamed to start with `list` (e.g. `get_versions` becomes `list_versions`). A warning diagnostic is displayed when such a rename occurs. +- Sub-clients that specify a `@clientName` decorator will use that client name verbatim instead of having the parent client name as a prefix. ### Bug Fixes -* Client constructors will now return an error if the endpoint parameter doesn't start with `http[s]`. -* Added support for unsigned integer types. -* Preserve `pub(crate)` on sub-client fields that can also be individually initialized. -* Removed redundant client accessor parameters that can be inherited from the parent client. +- Client constructors will now return an error if the endpoint parameter doesn't start with `http[s]`. +- Added support for unsigned integer types. +- Preserve `pub(crate)` on sub-client fields that can also be individually initialized. +- Removed redundant client accessor parameters that can be inherited from the parent client. ### Other Changes -* Updated to the latest tsp toolset. -* Set minimum node engine to `v18.x`. +- Updated to the latest tsp toolset. +- Set minimum node engine to `v18.x`. ## 0.10.0 (2025-02-25) ### Breaking Changes -* Model fields of type `HashMap` or `Vec` are no longer wrapped in an `Option`. +- Model fields of type `HashMap` or `Vec` are no longer wrapped in an `Option`. ### Features Added -* Added response types/traits for methods that return typed headers. +- Added response types/traits for methods that return typed headers. ### Other Changes -* Updated to the latest tsp toolset. +- Updated to the latest tsp toolset. ## 0.9.1 (2025-02-12) ### Bugs Fixed -* Added support for `enumvalue` types in method parameters. +- Added support for `enumvalue` types in method parameters. ## 0.9.0 (2025-02-10) ### Breaking Changes -* All client method option types are now exported from the `models` module (they are no longer in the root). +- All client method option types are now exported from the `models` module (they are no longer in the root). ### Features Added -* Merge preexisting `lib.rs` content with generated content. +- Merge preexisting `lib.rs` content with generated content. ### Other Changes -* Fixed formatting of some doc comments. - * HTML elements are converted to markdown equivalents. - * Bare URLs are converted to Rust docs hyperlinks. -* The emitter will attempt to execute `cargo fmt` after files are written. -* Add `derive` feature for `typespec_client_core` dependency. +- Fixed formatting of some doc comments. + - HTML elements are converted to markdown equivalents. + - Bare URLs are converted to Rust docs hyperlinks. +- The emitter will attempt to execute `cargo fmt` after files are written. +- Add `derive` feature for `typespec_client_core` dependency. ## 0.8.2 (2025-02-04) ### Other Changes -* Added various missing doc comments. +- Added various missing doc comments. ## 0.8.1 (2025-02-03) ### Bug Fixes -* Fixed bad codegen for certain cases of enum names. +- Fixed bad codegen for certain cases of enum names. ## 0.8.0 (2025-02-03) ### Breaking Changes -* Required `String` parameters are now emitted as `&str`. -* Sub-client modules are no longer publicly exported. - * All clients and their option types (client and/or method) are now exported in the `clients` module. - * Instantiable clients and their client options types along with all client method options will be re-exported in the crate's root. +- Required `String` parameters are now emitted as `&str`. +- Sub-client modules are no longer publicly exported. + - All clients and their option types (client and/or method) are now exported in the `clients` module. + - Instantiable clients and their client options types along with all client method options will be re-exported in the crate's root. ### Bugs Fixed -* Ensure that the API version query parameter in a pager's next link is set to the version on the client. +- Ensure that the API version query parameter in a pager's next link is set to the version on the client. ### Other Changes -* Input models are no longer `non_exhaustive`. -* Models and options types derive `SafeDebug` instead of `Debug`. +- Input models are no longer `non_exhaustive`. +- Models and options types derive `SafeDebug` instead of `Debug`. ## 0.7.0 (2025-01-17) ### Breaking Changes -* Methods that take a binary body now take a `RequestContent` instead of `RequestContent>`. -* Methods that return a binary body now return a `Response` instead of `Response<()>`. -* Client accessor methods now include any modeled parameters. +- Methods that take a binary body now take a `RequestContent` instead of `RequestContent>`. +- Methods that return a binary body now return a `Response` instead of `Response<()>`. +- Client accessor methods now include any modeled parameters. ### Bugs Fixed -* Use `serde` helpers to encode/decode time types in the specified wire format. +- Use `serde` helpers to encode/decode time types in the specified wire format. ### Other Changes -* Various codegen changes to clean up Clippy issues. -* Updated to the latest tsp toolset. +- Various codegen changes to clean up Clippy issues. +- Updated to the latest tsp toolset. ## 0.6.0 (2025-01-08) ### Breaking Changes -* Models and enums used as output types no longer implement `TryFrom`. Use `into_body()` instead of `try_into()` when deserializing a modeled response. +- Models and enums used as output types no longer implement `TryFrom`. Use `into_body()` instead of `try_into()` when deserializing a modeled response. ### Bugs Fixed -* Add `derive` and `xml` features in `Cargo.toml` files as required. -* Borrow client fields used in method header parameters if their type is non-copyable. +- Add `derive` and `xml` features in `Cargo.toml` files as required. +- Borrow client fields used in method header parameters if their type is non-copyable. ### Features Added -* Added support for TypeSpec `duration` types. Numeric durations are emitted as their respective types. For ISO8601 they're emitted as `String` types. +- Added support for TypeSpec `duration` types. Numeric durations are emitted as their respective types. For ISO8601 they're emitted as `String` types. ### Other Changes -* Removed dependency on crate `async-std`. +- Removed dependency on crate `async-std`. ## 0.5.1 (2024-12-19) ### Bugs Fixed -* Fixed bad codegen for enum values that contain a comma character. +- Fixed bad codegen for enum values that contain a comma character. ### Features Added -* Added support for model properties of type `path`. -* Aggregate inherited model properties so they're all in the super-type. +- Added support for model properties of type `path`. +- Aggregate inherited model properties so they're all in the super-type. ### Other Fixes -* Various codegen changes to clean up Clippy issues. +- Various codegen changes to clean up Clippy issues. ## 0.5.0 (2024-12-19) ### Breaking Changes -* Updated serde helpers to use renamed methods from core. This requires core versions from commit `65917ad` or later. +- Updated serde helpers to use renamed methods from core. This requires core versions from commit `65917ad` or later. ## 0.4.1 (2024-12-19) ### Bugs Fixed -* Fixed an issue that could cause incorrect usage of client parameters in method bodies. +- Fixed an issue that could cause incorrect usage of client parameters in method bodies. ### Features Added -* Added support for endpoints with supplemental paths. -* Added support for `OAuth2` credentials when part of a union authentication scheme. Unsupported schemes are omitted. +- Added support for endpoints with supplemental paths. +- Added support for `OAuth2` credentials when part of a union authentication scheme. Unsupported schemes are omitted. ### Other Changes -* Use `Url::join` for constructing the complete endpoint. -* Updated to the latest tsp toolset. +- Use `Url::join` for constructing the complete endpoint. +- Updated to the latest tsp toolset. ## 0.4.0 (2024-12-10) ### Breaking Changes -* `Azure.Core.eTag` types are now emitted as `azure_core::Etag` types. +- `Azure.Core.eTag` types are now emitted as `azure_core::Etag` types. ### Bugs Fixed -* Pager callbacks will properly clone method options when it contains non-copyable types. +- Pager callbacks will properly clone method options when it contains non-copyable types. ### Features Added -* Added support for required client parameters. +- Added support for required client parameters. ### Other Changes -* Methods create their own `Context` using the caller's as the parent. -* Updated to the latest version of `azure_core` which removed `AsClientMethodOptions` and it associated methods. +- Methods create their own `Context` using the caller's as the parent. +- Updated to the latest version of `azure_core` which removed `AsClientMethodOptions` and it associated methods. ## 0.3.0 (2024-12-06) ### Breaking Changes -* Model fields of type `url` are now emitted as `String` types. +- Model fields of type `url` are now emitted as `String` types. ### Bugs Fixed -* Fixed an issue that could cause a crash with error `Error: didn't find body format for model Error`. +- Fixed an issue that could cause a crash with error `Error: didn't find body format for model Error`. ### Other Changes -* Don't overwrite an existing `Cargo.toml` file by default. - * Specify `overwrite-cargo-toml=true` to force overwriting the file. -* Emitter args `crate-name` and `crate-version` have been marked as required. -* Updated minimum tcgc to `v0.48.4`. +- Don't overwrite an existing `Cargo.toml` file by default. + - Specify `overwrite-cargo-toml=true` to force overwriting the file. +- Emitter args `crate-name` and `crate-version` have been marked as required. +- Updated minimum tcgc to `v0.48.4`. ### Features Added -* Clients have an `endpoint()` method that returns its `azure_core::Url`. +- Clients have an `endpoint()` method that returns its `azure_core::Url`. ## 0.2.0 (2024-12-03) ### Breaking Changes -* Optional client method parameters are now in the method's options type. -* Sub-clients now have the suffix `Client` on their type names. -* Methods parameters of type `impl Into` have been changed to `String`. -* Client and method options builders have been removed. The options are now POD types. +- Optional client method parameters are now in the method's options type. +- Sub-clients now have the suffix `Client` on their type names. +- Methods parameters of type `impl Into` have been changed to `String`. +- Client and method options builders have been removed. The options are now POD types. ### Bugs Fixed -* Add necessary calls to `to_string()` for header/path/query params. -* Fixed improperly clearing an endpoint's query parameters during client construction. -* Fixed constructing URLs from routes that contain query parameters. -* Fixed handling of spread parameters when the param and serde names are different. +- Add necessary calls to `to_string()` for header/path/query params. +- Fixed improperly clearing an endpoint's query parameters during client construction. +- Fixed constructing URLs from routes that contain query parameters. +- Fixed handling of spread parameters when the param and serde names are different. ### Features Added -* Models now derive `typespec_client_core::Model`. -* Added support for binary responses. -* Added support for TypeSpec spread parameters. -* Added support for pageable methods. -* Added support for XML payloads. -* Added partial support for base64 encoded values. - * Headers, query parameters, and struct fields work. The exception for struct fields is nested arrays (e.g. `Vec>`). - * Requests and responses of base64 encoded values do not work due to the orphan problem. -* Added support for `x-ms-meta-*` headers in blob storage. +- Models now derive `typespec_client_core::Model`. +- Added support for binary responses. +- Added support for TypeSpec spread parameters. +- Added support for pageable methods. +- Added support for XML payloads. +- Added partial support for base64 encoded values. + - Headers, query parameters, and struct fields work. The exception for struct fields is nested arrays (e.g. `Vec>`). + - Requests and responses of base64 encoded values do not work due to the orphan problem. +- Added support for `x-ms-meta-*` headers in blob storage. ### Other Changes -* Use macros from `typespec_client_core` for creating enums. -* `TryFrom` implementations return an `azure_core::Result` instead of `std::result::Result`. -* Client parameters of type `impl AsRef` have been changed to `&str`. +- Use macros from `typespec_client_core` for creating enums. +- `TryFrom` implementations return an `azure_core::Result` instead of `std::result::Result`. +- Client parameters of type `impl AsRef` have been changed to `&str`. ## 0.1.0 (2024-10-10) -* Initial release +- Initial release diff --git a/packages/typespec-rust/README.md b/packages/typespec-rust/README.md index 9c5026aeb..2d58e5b42 100644 --- a/packages/typespec-rust/README.md +++ b/packages/typespec-rust/README.md @@ -4,7 +4,7 @@ The TypeSpec Rust generator is intended to be used with the TypeSpec compiler. ## Contributing -This project welcomes contributions and suggestions. Most contributions require you to agree to a +This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit . diff --git a/packages/typespec-rust/eslint.config.js b/packages/typespec-rust/eslint.config.js index 236d293e0..0e509f512 100644 --- a/packages/typespec-rust/eslint.config.js +++ b/packages/typespec-rust/eslint.config.js @@ -19,13 +19,8 @@ export default tseslint.config([ }, ], rules: { - indent: [ - 'warn', - 2, - { - SwitchCase: 1, - }, - ], + // Disable indent rule to avoid conflicts with Prettier + indent: 'off', '@typescript-eslint/no-empty-object-type': [ 'error', { diff --git a/packages/typespec-rust/package.json b/packages/typespec-rust/package.json index 9e0045ae4..84cc8b2ed 100644 --- a/packages/typespec-rust/package.json +++ b/packages/typespec-rust/package.json @@ -25,7 +25,13 @@ "test-ci": "vitest run --coverage --reporter=junit --reporter=default --no-file-parallelism", "tspcompile": "node .scripts/tspcompile.js", "watch": "tsc -p . --watch", - "workspace-members": "node .scripts/workspace-members.js" + "workspace-members": "node .scripts/workspace-members.js", + "format": "pnpm run format:prettier && pnpm run format:eslint", + "format:prettier": "prettier --write \"**/*.{md,json,ts,js}\" --ignore-path ../../.gitignore --ignore-path ../../.prettierignore", + "format:eslint": "eslint src test --fix --max-warnings=0", + "format:check": "pnpm run format:check:prettier && pnpm run format:check:eslint", + "format:check:prettier": "prettier --check \"**/*.{md,json,ts,js}\" --ignore-path ../../.gitignore --ignore-path ../../.prettierignore", + "format:check:eslint": "eslint src test --max-warnings=0" }, "files": [ "dist/src/**", @@ -67,6 +73,7 @@ "@vitest/coverage-v8": "^3.1.1", "@vitest/ui": "^3.1.1", "c8": "^10.1.3", + "prettier": "^3.0.0", "eslint": "^9.24.0", "typescript": "^5.8.3", "typescript-eslint": "^8.30.1", diff --git a/packages/typespec-rust/pnpm-lock.yaml b/packages/typespec-rust/pnpm-lock.yaml index 17a5cdf4b..caf3baedd 100644 --- a/packages/typespec-rust/pnpm-lock.yaml +++ b/packages/typespec-rust/pnpm-lock.yaml @@ -87,6 +87,9 @@ importers: eslint: specifier: ^9.24.0 version: 9.31.0 + prettier: + specifier: ^3.0.0 + version: 3.5.3 typescript: specifier: ^5.8.3 version: 5.8.3 diff --git a/packages/typespec-rust/src/codegen/cargotoml.ts b/packages/typespec-rust/src/codegen/cargotoml.ts index a7b95e593..3718b9fd7 100644 --- a/packages/typespec-rust/src/codegen/cargotoml.ts +++ b/packages/typespec-rust/src/codegen/cargotoml.ts @@ -1,13 +1,13 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ import * as rust from '../codemodel/index.js'; /** * emits the Cargo.toml file for the provided crate - * + * * @param crate the crate for which to emit a Cargo.toml file * @returns the contents of the Cargo.toml file */ @@ -22,7 +22,13 @@ export function emitCargoToml(crate: rust.Crate): string { content += '\n[dependencies]\n'; for (const dependency of crate.dependencies) { // dependency versions are managed by the workspace's Cargo.toml file - const features = dependency.features.length > 0 ? `, features = [${dependency.features.sort().map(f => `"${f}"`).join(', ')}]` : ''; + const features = + dependency.features.length > 0 + ? `, features = [${dependency.features + .sort() + .map((f) => `"${f}"`) + .join(', ')}]` + : ''; content += `${dependency.name} = { workspace = true${features} }\n`; } } diff --git a/packages/typespec-rust/src/codegen/clients.ts b/packages/typespec-rust/src/codegen/clients.ts index 7ba73ecb0..ae678fb47 100644 --- a/packages/typespec-rust/src/codegen/clients.ts +++ b/packages/typespec-rust/src/codegen/clients.ts @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ // cspell: ignore conv @@ -25,7 +25,7 @@ export interface ClientModules { /** * 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 */ @@ -38,9 +38,13 @@ export function emitClients(crate: rust.Crate): ClientModules | undefined { const clientOptionsImplDefault = function (constructable: rust.ClientConstruction): boolean { // only implement Default when there's more than one field (i.e. more than just client_options) // and the field(s) contain a client default value. - return constructable.options.type.fields.length > 1 && values(constructable.options.type.fields) - .where((field) => field.name !== 'client_options') - .where((field) => field.defaultValue !== undefined).any(); + return ( + constructable.options.type.fields.length > 1 && + values(constructable.options.type.fields) + .where((field) => field.name !== 'client_options') + .where((field) => field.defaultValue !== undefined) + .any() + ); }; const clientModules = new Array(); @@ -102,7 +106,7 @@ export function emitClients(crate: rust.Crate): ClientModules | undefined { body += `${indent.get()}${helpers.buildIfBlock(indent, { condition: `!${endpointParamName}.scheme().starts_with("http")`, body: (indent) => `${indent.get()}return Err(azure_core::Error::message(azure_core::error::ErrorKind::Other, format!("{${endpointParamName}} must use http(s)")));\n`, - })}` + })}`; body += `${indent.get()}${endpointParamName}.set_query(None);\n`; // construct the supplemental path and join it to the endpoint @@ -127,9 +131,13 @@ export function emitClients(crate: rust.Crate): ClientModules | undefined { // propagate the required client params to the initializer // NOTE: we do this on a sorted copy of the client params as we must preserve their order. // exclude endpoint params as they aren't propagated to clients (they're consumed when creating the complete endpoint) - const sortedParams = values([...constructor.params] - .sort((a: rust.ClientParameter, b: rust.ClientParameter) => { return helpers.sortAscending(a.name, b.name); })) - .where((each) => each.kind !== 'clientEndpoint').toArray(); + const sortedParams = values( + [...constructor.params].sort((a: rust.ClientParameter, b: rust.ClientParameter) => { + return helpers.sortAscending(a.name, b.name); + }), + ) + .where((each) => each.kind !== 'clientEndpoint') + .toArray(); for (const param of sortedParams) { if (param.optional) { @@ -139,7 +147,11 @@ export function emitClients(crate: rust.Crate): ClientModules | undefined { continue; } - if (!client.fields.find((v: rust.StructField) => { return v.name === param.name; })) { + if ( + !client.fields.find((v: rust.StructField) => { + return v.name === param.name; + }) + ) { throw new CodegenError('InternalError', `didn't find field in client ${client.name} for param ${param.name}`); } @@ -154,11 +166,19 @@ export function emitClients(crate: rust.Crate): ClientModules | undefined { continue; } - if (!client.fields.find((v: rust.StructField) => { return v.name === param.name; })) { + if ( + !client.fields.find((v: rust.StructField) => { + return v.name === param.name; + }) + ) { throw new CodegenError('InternalError', `didn't find field in client ${client.name} for param ${param.name}`); } - if (!client.constructable.options.type.fields.find((v: rust.StructField) => { return v.name === param.name; })) { + if ( + !client.constructable.options.type.fields.find((v: rust.StructField) => { + return v.name === param.name; + }) + ) { throw new CodegenError('InternalError', `didn't find field in client options ${client.constructable.options.type.name} for optional param ${param.name}`); } @@ -343,7 +363,7 @@ function getMethodOptions(crate: rust.Crate): helpers.Module { /** * builds the block of doc comments for a callable's parameters. * if the callable has no parameters, undefined is returned. - * + * * @param indent the indentation helper currently in scope * @param callable the callable containing parameters to document * @returns the parameters doc comments or undefined @@ -394,7 +414,7 @@ function getParamsBlockDocComment(indent: helpers.indentation, callable: rust.Co /** * creates the parameter signature for a client constructor * e.g. "foo: i32, bar: String, options: ClientOptions" - * + * * @param params the params to include in the signature. can be empty * @param options the client options type. will always be the last parameter * @param use the use statement builder currently in scope @@ -418,7 +438,7 @@ function getConstructorParamsSig(params: Array, options: r /** * creates the parameter signature for a client method * e.g. "foo: i32, bar: String, options: MethodOptions" - * + * * @param method the Rust method for which to create the param sig * @param use the use statement builder currently in scope * @returns the method params sig @@ -462,7 +482,7 @@ function getMethodParamsSig(method: rust.MethodType, use: Use): string { /** * returns the auth policy instantiation code if the ctor contains a credential param. * the policy will be a local var named auth_policy. - * + * * @param ctor the constructor for which to instantiate an auth policy * @param use the use statement builder currently in scope * @returns the auth policy instantiation code or undefined if not required @@ -485,7 +505,7 @@ function getAuthPolicy(ctor: rust.Constructor, use: Use): string | undefined { /** * returns the complete text for the provided parameter's type * e.g. self, &String, mut SomeStruct - * + * * @param param the parameter for which to create the * @returns the parameter's type declaration */ @@ -513,7 +533,7 @@ function formatParamTypeName(param: rust.MethodParameter | rust.Parameter | rust /** * returns the name of the endpoint field within the client - * + * * @param client the client in which to find the endpoint field * @returns the name of the endpoint field */ @@ -537,7 +557,7 @@ function getEndpointFieldName(client: rust.Client): string { /** * constructs the body for a client accessor method - * + * * @param indent the indentation helper currently in scope * @param clientAccessor the client accessor for which to construct the body * @returns the contents of the method body @@ -600,7 +620,7 @@ interface MethodParamGroups { /** * enumerates method parameters and returns them based on groups - * + * * @param method the method containing the parameters to group * @returns the groups parameters */ @@ -638,9 +658,15 @@ function getMethodParamGroup(method: ClientMethod): MethodParamGroups { } } - headerParams.sort((a: HeaderParamType, b: HeaderParamType) => { return helpers.sortAscending(a.header, b.header); }); - pathParams.sort((a: PathParamType, b: PathParamType) => { return helpers.sortAscending(a.segment, b.segment); }); - queryParams.sort((a: QueryParamType, b: QueryParamType) => { return helpers.sortAscending(a.key, b.key); }); + headerParams.sort((a: HeaderParamType, b: HeaderParamType) => { + return helpers.sortAscending(a.header, b.header); + }); + pathParams.sort((a: PathParamType, b: PathParamType) => { + return helpers.sortAscending(a.segment, b.segment); + }); + queryParams.sort((a: QueryParamType, b: QueryParamType) => { + return helpers.sortAscending(a.key, b.key); + }); let bodyParam: rust.BodyParameter | undefined; for (const param of method.params) { @@ -665,23 +691,25 @@ function getMethodParamGroup(method: ClientMethod): MethodParamGroups { /** * wraps the emitted code emitted by setter in a "let Some" block * if the parameter is optional, else the value of setter is returned. - * + * * NOTE: for optional params, by convention, we'll create a local named param.name. * setter MUST reference by param.name so it works for optional and required params. - * + * * @param indent the indentation helper currently in scope * @param param the parameter to which the contents of setter apply * @param setter the callback that emits the code to read from a param var * @param inClosure indicates if the value is being read from within a closure (e.g. pageable methods) - * @returns + * @returns */ function getParamValueHelper(indent: helpers.indentation, param: rust.MethodParameter, inClosure: boolean, setter: () => string): string { if (param.optional) { // optional params are in the unwrapped options local var - const op = indent.get() + helpers.buildIfBlock(indent, { - condition: `let Some(${param.name}) = ${inClosure ? '&' : ''}options.${param.name}`, - body: setter, - }); + const op = + indent.get() + + helpers.buildIfBlock(indent, { + condition: `let Some(${param.name}) = ${inClosure ? '&' : ''}options.${param.name}`, + body: setter, + }); return op + '\n'; } return setter(); @@ -689,7 +717,7 @@ function getParamValueHelper(indent: helpers.indentation, param: rust.MethodPara /** * emits the code for building the request URL. - * + * * @param indent the indentation helper currently in scope * @param use the use statement builder currently in scope * @param method the method for which we're building the body @@ -733,39 +761,35 @@ function constructUrl(indent: helpers.indentation, use: Use, method: ClientMetho let wrapSortedVec: (s: string) => string = (s) => s; let paramExpression = `${borrowOrNot(pathParam)}${getHeaderPathQueryParamValue(use, pathParam, true)}`; if (pathParam.kind === 'pathHashMap') { - wrapSortedVec = (s) => `${indent.get()}{` - + `${indent.push().get()}let mut ${pathParam.name}_vec = ${pathParam.name}.iter().collect::>();\n` - + `${indent.get()}${pathParam.name}_vec.sort_by_key(|p| p.0);\n` - + `${s}` - + `${indent.pop().get()}}`; + wrapSortedVec = (s) => + `${indent.get()}{` + + `${indent.push().get()}let mut ${pathParam.name}_vec = ${pathParam.name}.iter().collect::>();\n` + + `${indent.get()}${pathParam.name}_vec.sort_by_key(|p| p.0);\n` + + `${s}` + + `${indent.pop().get()}}`; const kEqualsV = '"{k}={v}"'; const kCommaV = '"{k},{v}"'; - paramExpression = `&${pathParam.name}_vec.iter().map(|(k,v)| ` - + (pathParam.explode - ? `format!(${kEqualsV})).collect::>().join(",")` - : `format!(${kCommaV})).collect::>().join(",")`); + paramExpression = + `&${pathParam.name}_vec.iter().map(|(k,v)| ` + + (pathParam.explode ? `format!(${kEqualsV})).collect::>().join(",")` : `format!(${kCommaV})).collect::>().join(",")`); switch (pathParam.style) { case 'path': - paramExpression = `&format!("/{}", ${pathParam.name}_vec.iter().map(|(k,v)| ` - + (pathParam.explode - ? `format!(${kEqualsV})).collect::>().join("/"))` - : `format!(${kCommaV})).collect::>().join(","))`); + paramExpression = + `&format!("/{}", ${pathParam.name}_vec.iter().map(|(k,v)| ` + + (pathParam.explode ? `format!(${kEqualsV})).collect::>().join("/"))` : `format!(${kCommaV})).collect::>().join(","))`); break; case 'label': - paramExpression = `&format!(".{}", ${pathParam.name}_vec.iter().map(|(k,v)| ` - + (pathParam.explode - ? `format!(${kEqualsV})).collect::>().join("."))` - : `format!(${kCommaV})).collect::>().join(","))`); + paramExpression = + `&format!(".{}", ${pathParam.name}_vec.iter().map(|(k,v)| ` + + (pathParam.explode ? `format!(${kEqualsV})).collect::>().join("."))` : `format!(${kCommaV})).collect::>().join(","))`); break; case 'matrix': paramExpression = pathParam.explode - ? (`&format!(";{}", ${pathParam.name}_vec.into_iter().map(|(k,v)| ` - + `format!(${kEqualsV})).collect::>().join(";"))`) - : (`&format!(";${pathParam.name}={}", ${pathParam.name}_vec.into_iter().map(|(k,v)| ` - + `format!(${kCommaV})).collect::>().join(","))`); + ? `&format!(";{}", ${pathParam.name}_vec.into_iter().map(|(k,v)| ` + `format!(${kEqualsV})).collect::>().join(";"))` + : `&format!(";${pathParam.name}={}", ${pathParam.name}_vec.into_iter().map(|(k,v)| ` + `format!(${kCommaV})).collect::>().join(","))`; break; } } else if (pathParam.kind === 'pathCollection') { @@ -778,8 +802,7 @@ function constructUrl(indent: helpers.indentation, use: Use, method: ClientMetho paramExpression = `&format!(".{}", ${pathParam.name}.join("${pathParam.explode ? '.' : ','}"))`; break; case 'matrix': - paramExpression = `&format!(";${pathParam.name}={}", ${pathParam.name}.join(` - + `"${pathParam.explode ? `;${pathParam.name}=` : ','}"))`; + paramExpression = `&format!(";${pathParam.name}={}", ${pathParam.name}.join(` + `"${pathParam.explode ? `;${pathParam.name}=` : ','}"))`; break; } } else { @@ -864,7 +887,7 @@ function constructUrl(indent: helpers.indentation, use: Use, method: ClientMetho * emits the code for building the HTTP request. * assumes that there's a local var 'url' which is the Url. * creates a mutable local 'request' which is the Request instance. - * + * * @param indent the indentation helper currently in scope * @param use the use statement builder currently in scope * @param method the method for which we're building the body @@ -883,14 +906,21 @@ function constructRequest(indent: helpers.indentation, use: Use, method: ClientM continue; } - if (method.kind === 'pageable' && method.strategy?.kind === 'continuationToken' && method.strategy?.requestToken.kind === 'headerScalar' && method.strategy?.requestToken === headerParam) { + if ( + method.kind === 'pageable' && + method.strategy?.kind === 'continuationToken' && + method.strategy?.requestToken.kind === 'headerScalar' && + method.strategy?.requestToken === headerParam + ) { // we have some special handling for the header continuation token. // if we have a token value, i.e. from the next page, then use that value. // if not, then check if an optional token value was provided. - body += indent.get() + helpers.buildIfBlock(indent, { - condition: `let Some(${headerParam.name}) = ${headerParam.name}.or_else(|| options.${headerParam.name}.clone())`, - body: (indent) => `${indent.get()}request.insert_header("${headerParam.header}", ${headerParam.name});\n`, - }) + body += + indent.get() + + helpers.buildIfBlock(indent, { + condition: `let Some(${headerParam.name}) = ${headerParam.name}.or_else(|| options.${headerParam.name}.clone())`, + body: (indent) => `${indent.get()}request.insert_header("${headerParam.header}", ${headerParam.name});\n`, + }); continue; } @@ -926,7 +956,10 @@ function constructRequest(indent: helpers.indentation, use: Use, method: ClientM indent.push(); for (const partialBodyParam of paramGroups.partialBody) { if (partialBodyParam.type.content.type !== requestContentType.content.type) { - throw new CodegenError('InternalError', `spread param ${partialBodyParam.name} has conflicting model type ${partialBodyParam.type.content.type.name}, expected model type ${requestContentType.content.type.name}`); + throw new CodegenError( + 'InternalError', + `spread param ${partialBodyParam.name} has conflicting model type ${partialBodyParam.type.content.type.name}, expected model type ${requestContentType.content.type.name}`, + ); } if (partialBodyParam.optional) { @@ -966,7 +999,7 @@ function errIfNotSuccessResponse(use: Use, indent: helpers.indentation): string return helpers.buildIfBlock(indent, { condition: '!rsp.status().is_success()', body: (indent) => { - let body = `${indent.get()}let status = rsp.status();\n` + let body = `${indent.get()}let status = rsp.status();\n`; body += `${indent.get()}let http_error = HttpError::new(rsp).await;\n`; body += `${indent.get()}let error_kind = ErrorKind::http_response(status, http_error.error_code().map(std::borrow::ToOwned::to_owned));\n`; body += `${indent.get()}return Err(Error::new(error_kind, http_error));\n`; @@ -990,7 +1023,7 @@ function urlVarNeedsMut(paramGroups: MethodParamGroups, method: ClientMethod): s /** * constructs the body for an async client method - * + * * @param indent the indentation helper currently in scope * @param use the use statement builder currently in scope * @param client the client to which the method belongs @@ -1019,7 +1052,7 @@ function getAsyncMethodBody(indent: helpers.indentation, use: Use, client: rust. /** * constructs the body for a pageable client method - * + * * @param indent the indentation helper currently in scope * @param use the use statement builder currently in scope * @param client the client to which the method belongs @@ -1062,46 +1095,53 @@ function getPageableMethodBody(indent: helpers.indentation, use: Use, client: ru body += `${indent.get()}${helpers.buildIfBlock(indent, { condition: `let Some(${reqTokenParam}) = ${reqTokenParam}`, body: (indent) => { - let body = indent.get() + helpers.buildIfBlock(indent, { - condition: `url.query_pairs().any(|(name, _)| name.eq("${reqTokenValue}"))`, - body: (indent) => { - let body = `${indent.get()}let mut new_url = url.clone();\n`; - body += `${indent.get()}new_url.query_pairs_mut().clear().extend_pairs(url.query_pairs().filter(|(name, _)| name.ne("${reqTokenValue}")));\n`; - body += `${indent.get()}url = new_url;\n`; - return body; - }, - }); + let body = + indent.get() + + helpers.buildIfBlock(indent, { + condition: `url.query_pairs().any(|(name, _)| name.eq("${reqTokenValue}"))`, + body: (indent) => { + let body = `${indent.get()}let mut new_url = url.clone();\n`; + body += `${indent.get()}new_url.query_pairs_mut().clear().extend_pairs(url.query_pairs().filter(|(name, _)| name.ne("${reqTokenValue}")));\n`; + body += `${indent.get()}url = new_url;\n`; + return body; + }, + }); body += `${indent.get()}url.query_pairs_mut().append_pair("${reqTokenValue}", &${reqTokenParam});\n`; return body; - } - })}` + }, + })}`; } break; } case 'nextLink': { const nextLinkName = method.strategy.nextLink.name; body += `${indent.get()}Ok(${method.returns.type.name}::from_callback(move |${nextLinkName}: Option| {\n`; - body += `${indent.push().get()}let url = ` + helpers.buildMatch(indent, nextLinkName, [{ - pattern: `Some(${nextLinkName})`, - body: (indent) => { - if (paramGroups.apiVersion && paramGroups.apiVersion.kind === 'queryScalar') { - const apiVersionKey = `"${paramGroups.apiVersion.key}"`; - // there are no APIs to set/update an existing query parameter. - // so, we filter the existing query params to remove the api-version - // query param. we then add back the filtered set and then add the - // api-version as specified on the client. - let setApiVerBody = `${indent.get()}let qp = ${nextLinkName}.query_pairs().filter(|(name, _)| name.ne(${apiVersionKey}));\n`; - setApiVerBody += `${indent.get()}let mut ${nextLinkName} = ${nextLinkName}.clone();\n`; - setApiVerBody += `${indent.get()}${nextLinkName}.query_pairs_mut().clear().extend_pairs(qp).append_pair(${apiVersionKey}, &${paramGroups.apiVersion.name});\n`; - setApiVerBody += `${indent.get()}${nextLinkName}\n`; - return setApiVerBody; - } - return `${indent.get()} ${nextLinkName}\n`; - } - }, { - pattern: 'None', - body: (indent) => `${indent.get()}${urlVar}.clone()\n` - }]); + body += + `${indent.push().get()}let url = ` + + helpers.buildMatch(indent, nextLinkName, [ + { + pattern: `Some(${nextLinkName})`, + body: (indent) => { + if (paramGroups.apiVersion && paramGroups.apiVersion.kind === 'queryScalar') { + const apiVersionKey = `"${paramGroups.apiVersion.key}"`; + // there are no APIs to set/update an existing query parameter. + // so, we filter the existing query params to remove the api-version + // query param. we then add back the filtered set and then add the + // api-version as specified on the client. + let setApiVerBody = `${indent.get()}let qp = ${nextLinkName}.query_pairs().filter(|(name, _)| name.ne(${apiVersionKey}));\n`; + setApiVerBody += `${indent.get()}let mut ${nextLinkName} = ${nextLinkName}.clone();\n`; + setApiVerBody += `${indent.get()}${nextLinkName}.query_pairs_mut().clear().extend_pairs(qp).append_pair(${apiVersionKey}, &${paramGroups.apiVersion.name});\n`; + setApiVerBody += `${indent.get()}${nextLinkName}\n`; + return setApiVerBody; + } + return `${indent.get()} ${nextLinkName}\n`; + }, + }, + { + pattern: 'None', + body: (indent) => `${indent.get()}${urlVar}.clone()\n`, + }, + ]); body += ';\n'; break; } @@ -1172,19 +1212,22 @@ function getPageableMethodBody(indent: helpers.indentation, use: Use, client: ru // we need to handle the case where the next page value is the empty string, // so checking strictly for None(theNextLink) is insufficient. // the most common case for this is XML, e.g. an empty tag like - body += `${indent.get()}Ok(${helpers.buildMatch(indent, srcNextPage, [{ - pattern: `Some(${nextPageValue}) if !${nextPageValue}.is_empty()`, - body: (indent) => { - return `${indent.get()}response: rsp, continuation: ${continuation}`; + body += `${indent.get()}Ok(${helpers.buildMatch(indent, srcNextPage, [ + { + pattern: `Some(${nextPageValue}) if !${nextPageValue}.is_empty()`, + body: (indent) => { + return `${indent.get()}response: rsp, continuation: ${continuation}`; + }, + returns: 'PagerResult::More', }, - returns: 'PagerResult::More', - }, { - pattern: '_', - body: (indent) => { - return `${indent.get()}response: rsp`; + { + pattern: '_', + body: (indent) => { + return `${indent.get()}response: rsp`; + }, + returns: 'PagerResult::Done', }, - returns: 'PagerResult::Done', - }])}`; + ])}`; body += ')\n'; // end Ok body += `${indent.pop().get()}}\n`; // end async move body += `${indent.pop().get()}}))`; // end Ok/Pager::from_callback @@ -1224,12 +1267,12 @@ function getClientEndpointParamValue(param: rust.ClientEndpointParameter): strin /** * contains the code to use when populating a header/path/query value * from a parameter of that type. - * + * * if the param's type is a String, then the return value is simply the * param's name. the non-String cases require some kind of conversion. * this could simply be a to_string() call, e.g. "paramName.to_string()". * other cases might be more complex. - * + * * @param use the use statement builder currently in scope * @param param the param for which to get the value * @param fromSelf applicable for client params. when true, the prefix "self." is included @@ -1317,7 +1360,7 @@ function getHeaderPathQueryParamValue(use: Use, param: HeaderParamType | PathPar /** * returns the delimiter character for the provided format type - * + * * @param format the format collection type * @returns the delimiter character */ diff --git a/packages/typespec-rust/src/codegen/codeGenerator.ts b/packages/typespec-rust/src/codegen/codeGenerator.ts index 76277f5d5..59ef3199f 100644 --- a/packages/typespec-rust/src/codegen/codeGenerator.ts +++ b/packages/typespec-rust/src/codegen/codeGenerator.ts @@ -40,7 +40,7 @@ export class CodeGenerator { /** * generates a Cargo.toml file - * + * * @returns the contents for the Cargo.toml file */ emitCargoToml(): string { @@ -49,7 +49,7 @@ export class CodeGenerator { /** * generates the lib.rs file for crate - * + * * @returns the content for lib.rs */ emitLibRs(): string { @@ -58,7 +58,7 @@ export class CodeGenerator { /** * generates all clients, models, and any helper content - * + * * @returns an array of files to emit */ emitContent(): Array { @@ -80,7 +80,11 @@ export class CodeGenerator { const clientModules = emitClients(this.crate); if (clientModules) { - files.push(...clientModules.modules.map((module) => { return { name: `${clientsSubDir}/${module.name}.rs`, content: module.content }; })); + 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, 'pubUse'); } @@ -97,7 +101,7 @@ export class CodeGenerator { addModelsFile(emitHeaderTraits(this.crate), 'pubUse'); if (modelsModRS.length > 0) { - files.push({ name: `${modelsSubDir}/mod.rs`, content: emitModelsModRs(modelsModRS) }) + files.push({ name: `${modelsSubDir}/mod.rs`, content: emitModelsModRs(modelsModRS) }); } // there will always be something in the generated/mod.rs file diff --git a/packages/typespec-rust/src/codegen/context.ts b/packages/typespec-rust/src/codegen/context.ts index 2820f50ea..43b5cbd3b 100644 --- a/packages/typespec-rust/src/codegen/context.ts +++ b/packages/typespec-rust/src/codegen/context.ts @@ -20,7 +20,7 @@ export class Context { /** * instantiates a new Context for the provided crate - * + * * @param crate the crate for which the context will be constructed */ constructor(crate: rust.Crate) { @@ -93,7 +93,7 @@ export class Context { /** * returns the impl TryFrom for RequestContent where T is type. * if no impl is required, it returns undefined. - * + * * @param model the model for which to implement TryFrom * @param use the use statement builder currently in scope * @returns the impl TryFrom block for type or undefined @@ -120,7 +120,7 @@ export class Context { /** * returns the body format for the provided model - * + * * @param model the model for which to determine the format * @returns the body format */ @@ -139,7 +139,7 @@ export class Context { /** * returns an azure_core::http::Page impl for the provided model * or undefined if the model isn't a paged response type. - * + * * @param model the model for which to create the Page impl * @param use the use statement builder currently in scope * @returns the Page impl or undefined diff --git a/packages/typespec-rust/src/codegen/enums.ts b/packages/typespec-rust/src/codegen/enums.ts index af98c85db..b3d985830 100644 --- a/packages/typespec-rust/src/codegen/enums.ts +++ b/packages/typespec-rust/src/codegen/enums.ts @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ import { Context } from './context.js'; import * as helpers from './helpers.js'; @@ -11,7 +11,7 @@ import * as rust from '../codemodel/index.js'; /** * returns the emitted enum types, or undefined if the * crate contains no enum types. - * + * * @param crate the crate for which to emit enums * @param context the context for the provided crate * @returns the enum content or undefined diff --git a/packages/typespec-rust/src/codegen/errors.ts b/packages/typespec-rust/src/codegen/errors.ts index 7175c0c22..486bdb7be 100644 --- a/packages/typespec-rust/src/codegen/errors.ts +++ b/packages/typespec-rust/src/codegen/errors.ts @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ /** ErrorCode defines the types of errors */ export type ErrorCode = diff --git a/packages/typespec-rust/src/codegen/headerTraits.ts b/packages/typespec-rust/src/codegen/headerTraits.ts index 8b7fc8431..d1786b583 100644 --- a/packages/typespec-rust/src/codegen/headerTraits.ts +++ b/packages/typespec-rust/src/codegen/headerTraits.ts @@ -13,7 +13,7 @@ import * as rust from '../codemodel/index.js'; * returns the emitted header traits, or undefined if there * are no header traits. * the header traits provide access to typed response headers - * + * * @param crate the crate for which to emit header traits * @returns the header traits content or undefined */ @@ -45,7 +45,7 @@ export function emitHeaderTraits(crate: rust.Crate): helpers.Module | undefined /** adds response headers to headers, avoiding duplicates */ const addHeaders = function (...responseHeaders: Array): void { for (const responseHeader of responseHeaders) { - if (!headers.find(v => v.header === responseHeader.header)) { + if (!headers.find((v) => v.header === responseHeader.header)) { headers.push(responseHeader); } } @@ -66,7 +66,7 @@ export function emitHeaderTraits(crate: rust.Crate): helpers.Module | undefined mergedDocs += `/// * ${src.docs}\n`; for (const responseHeader of src.headers) { - const matchingHeader = mergedHeaders.find(h => h.header === responseHeader.header); + const matchingHeader = mergedHeaders.find((h) => h.header === responseHeader.header); if (!matchingHeader) { mergedHeaders.push(responseHeader); } else if (matchingHeader.type !== responseHeader.type) { @@ -176,20 +176,20 @@ export function emitHeaderTraits(crate: rust.Crate): helpers.Module | undefined /** * creates the name to use for a header constant * e.g. header Content-Type becomes CONTENT_TYPE - * + * * @param header the name of the header * @returns the header constant */ function getHeaderConstName(header: rust.ResponseHeader): string { // strip off any x-ms- prefix const chunks = codegen.deconstruct(header.header.replace(/^x-ms-/i, '')); - return `${chunks.map(i => i.toUpperCase()).join('_')}`; + return `${chunks.map((i) => i.toUpperCase()).join('_')}`; } /** * returns the body of a header trait method, * performing any deserialization as required. - * + * * @param indent the indentation helper currently in scope * @param header the header to deserialize * @returns the header method body @@ -220,7 +220,7 @@ function getHeaderDeserialization(indent: helpers.indentation, use: Use, header: case 'enum': case 'scalar': case 'String': - return `${indent.get()}Headers::get_optional_as(self.headers(), &${headerConstName})\n` + return `${indent.get()}Headers::get_optional_as(self.headers(), &${headerConstName})\n`; case 'offsetDateTime': { const timeParse = `parse_${header.type.encoding}`; use.add('azure_core', `time::${timeParse}`); @@ -233,7 +233,7 @@ function getHeaderDeserialization(indent: helpers.indentation, use: Use, header: /** * returns the mod private {...} section used to seal the header traits. - * + * * @param traitDefs the trait definitions to seal * @returns the private mod definition */ diff --git a/packages/typespec-rust/src/codegen/helpers.ts b/packages/typespec-rust/src/codegen/helpers.ts index 0297c3df0..27e828e15 100644 --- a/packages/typespec-rust/src/codegen/helpers.ts +++ b/packages/typespec-rust/src/codegen/helpers.ts @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ import * as codegen from '@azure-tools/codegen'; import { values } from '@azure-tools/linq'; @@ -16,7 +16,7 @@ const headerText = `// Copyright (c) Microsoft Corporation. All rights reserved. export const AnnotationNonExhaustive = '#[non_exhaustive]\n'; -export const AnonymousLifetimeAnnotation = '<\'_>'; +export const AnonymousLifetimeAnnotation = "<'_>"; /** a module to emit */ export interface Module { @@ -29,7 +29,7 @@ export interface Module { /** * returns the content preamble common to all emitted files - * + * * @param [includeDNE=true] controls if the phrase 'DO NOT EDIT' should be included * @returns the preamble content */ @@ -39,7 +39,7 @@ export function contentPreamble(includeDNE: boolean = true): string { /** * formats doc comments if available - * + * * @param docs contains any doc comments * @param forDocAttr indicates doc comments will be in a #[doc] attribute, thus omitting the /// prefixes (defaults to false) * @param prefix optional prefix to insert before the docs @@ -56,7 +56,7 @@ export function formatDocComment(docs: rust.Docs, forDocAttr = false, prefix?: s indentLevel = indent.get(); } - const commentLines = function(docs: string): string { + const commentLines = function (docs: string): string { let commentedLines = ''; const lines = docs.split('\n'); for (const line of lines) { @@ -80,7 +80,12 @@ export function formatDocComment(docs: rust.Docs, forDocAttr = false, prefix?: s // * foo - some comment first line // and it finishes here. const blockStartMatch = formattedLine.match(/^\/\/\/\s+([*-]|(?:\d+\.))/); - if (blockStartMatch && values(formattedLine).where((c) => c === '\n').count() > 0) { + if ( + blockStartMatch && + values(formattedLine) + .where((c) => c === '\n') + .count() > 0 + ) { const chunks = formattedLine.split('\n'); for (let i = 1; i < chunks.length; ++i) { // indent size is based on the size of the captured starting block minus 3 for the /// chars @@ -124,7 +129,7 @@ export function formatDocComment(docs: rust.Docs, forDocAttr = false, prefix?: s /** * returns the specified visibility prefix - * + * * @param visibility the visibility to evaluate * @returns the prefix */ @@ -139,10 +144,10 @@ export function emitVisibility(visibility: rust.Visibility): string { /** * returns the type declaration string for the specified Rust type - * + * * @param type is the Rust type for which to emit the declaration * @param withAnonymousLifetime indicates if an existing lifetime annotation should be substituted with the anonymous lifetime - * @returns + * @returns */ export function getTypeDeclaration(type: rust.Client | rust.Payload | rust.ResponseHeadersTrait | rust.Type, withAnonymousLifetime = false): string { switch (type.kind) { @@ -219,7 +224,7 @@ export function getTypeDeclaration(type: rust.Client | rust.Payload | rust.Respo } return `${type.name}${getGenericLifetimeAnnotation(type.lifetime)}`; case 'unit': - return '()'; + return '()'; case 'Vec': return `${type.kind}<${getTypeDeclaration(type.type, withAnonymousLifetime)}>`; } @@ -242,7 +247,7 @@ export class indentation { /** * returns spaces for the current indentation level - * + * * @returns a string with the current indentation level */ get(): string { @@ -255,7 +260,7 @@ export class indentation { /** * increments the indentation level - * + * * @returns this indentation instance */ push(): indentation { @@ -265,7 +270,7 @@ export class indentation { /** * decrements the indentation level - * + * * @returns this indentation instance */ pop(): indentation { @@ -279,14 +284,14 @@ export class indentation { /** * emits the derive annotation with the standard and any additional values - * + * * @param extra contains any extra derive values * @returns a derive macro */ export function annotationDerive(...extra: Array): string { const derive = new Array('Clone', 'Deserialize', 'SafeDebug', 'Serialize'); // remove any empty values - extra = extra.filter(entry => entry.trim() !== ''); + extra = extra.filter((entry) => entry.trim() !== ''); derive.push(...extra); derive.sort(); return `#[derive(${derive.join(', ')})]\n`; @@ -294,7 +299,7 @@ export function annotationDerive(...extra: Array): string { /** * used to sort strings in ascending order - * + * * @param a is the value on the left side * @param b is the value on the right side * @returns -1 if a < b, 1 if a > b, or 0 if they're equal @@ -305,7 +310,7 @@ export function sortAscending(a: string, b: string): number { /** * returns the generic lifetime annotation string for lifetime (e.g. <'a>) - * + * * @param lifetime contains the Rust lifetime value * @returns the properly formatted lifetime annotation */ @@ -330,7 +335,7 @@ export interface elseBlock { /** * constructs an if block (can expand to include else if/else as necessary) - * + * * @param indent the current indentation helper in scope * @param ifBlock the if block definition * @param elseBlock optional else block definition @@ -364,7 +369,7 @@ export interface matchArm { /** * constructs a match expression at the provided indentation level - * + * * @param indent the current indentation helper in scope * @param expr the expression to match * @param arms one or more match arms @@ -388,7 +393,7 @@ export function buildMatch(indent: indentation, expr: string, arms: Array Foo - * + * * @param str the string to capitalize * @returns the capitalized value */ @@ -398,7 +403,7 @@ export function capitalize(str: string): string { /** * recursively unwraps a type. if type is an Option>, returns the T - * + * * @param type is the type to unwrap * @returns the wrapped type or the original type if it wasn't wrapped */ @@ -426,7 +431,7 @@ export type ModelFormat = 'json' | 'xml'; /** * converts a ResponseFormat to json or xml - * + * * @param format is the format to convert * @returns json or xml */ @@ -441,7 +446,7 @@ export function convertResponseFormat(format: Exclude): string { /** * emits the contents of the generated/mod.rs file - * + * * @param crate the crate for which to emit the mod.rs file * @returns the contents of the mod.rs file */ @@ -61,7 +61,7 @@ export function emitGeneratedModRs(crate: rust.Crate): string { /** * emits the contents of the models/mod.rs file - * + * * @param modules the modules to include * @returns the contents of the mod.rs file */ diff --git a/packages/typespec-rust/src/codegen/models.ts b/packages/typespec-rust/src/codegen/models.ts index a7f1150c5..736743dd2 100644 --- a/packages/typespec-rust/src/codegen/models.ts +++ b/packages/typespec-rust/src/codegen/models.ts @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ import * as codegen from '@azure-tools/codegen'; import { Context } from './context.js'; @@ -32,7 +32,7 @@ export interface Models { /** * returns the emitted model types, or empty if the * crate contains no model types. - * + * * @param crate the crate for which to emit models * @param context the context for the provided crate * @returns the model content or empty @@ -53,7 +53,7 @@ export function emitModels(crate: rust.Crate, context: Context): Models { /** * the implementation of emitModels - * + * * @param crate the crate for which to emit models * @param context the context for the provided crate * @param visibility the visibility of the models to emit @@ -182,7 +182,7 @@ function emitModelsInternal(crate: rust.Crate, context: Context, visibility: rus /** * returns serde helpers for public models. * if no helpers are required, undefined is returned. - * + * * @returns the model serde helpers content or undefined */ function emitModelsSerde(): helpers.Module | undefined { @@ -207,7 +207,7 @@ function emitModelsSerde(): helpers.Module | undefined { /** * returns any trait impls for public models. * if no helpers are required, undefined is returned. - * + * * @param crate the crate for which to emit model serde helpers * @param context the context for the provided crate * @returns the model serde helpers content or undefined @@ -257,7 +257,7 @@ function emitModelImpls(crate: rust.Crate, context: Context): helpers.Module | u /** * returns the value for the rename option in a serde derive macro * or undefined if no rename is required. - * + * * @param field the field for which to emit a rename * @returns the value for the rename option or undefined */ @@ -315,7 +315,7 @@ const xmlListWrappers = new Map(); /** * gets or creates an XMLListWrapper for the specified model field. * assumes that it's been determined that the wrapper is required. - * + * * @param field the field for which to create an XMLWrapper * @returns the XMLListWrapper for the provided field */ @@ -392,7 +392,7 @@ function getXMLListWrapper(field: rust.ModelField): XMLListWrapper { /** * emits helper types for XML lists or returns undefined * if no XMLListWrappers are required. - * + * * @returns the helper models for wrapped XML lists or undefined */ function emitXMLListWrappers(): helpers.Module | undefined { @@ -401,7 +401,9 @@ function emitXMLListWrappers(): helpers.Module | undefined { } const wrapperTypes = Array.from(xmlListWrappers.values()); - wrapperTypes.sort((a, b) => { return helpers.sortAscending(a.name, b.name); }); + wrapperTypes.sort((a, b) => { + return helpers.sortAscending(a.name, b.name); + }); const indent = new helpers.indentation(); const use = new Use('modelsOther'); @@ -463,7 +465,7 @@ const serdeHelpers = new Map(); /** * defines serde helpers for encodedBytes and offsetDateTime types. * any other type will cause this function to throw. - * + * * @param field the model field for which to build serde helpers * @param serdeParams the params that will be passed to the serde annotation * @param use the use statement builder currently in scope @@ -588,7 +590,7 @@ function getSerDeHelper(field: rust.ModelField, serdeParams: Set, use: U /** * emits serde helper modules or returns undefined * if no serde helpers are required. - * + * * @param use the use statement builder at the file scope * @returns the helper modules or undefined */ @@ -627,7 +629,7 @@ function emitSerDeHelpers(use: Use): string | undefined { /** * constructs a serde serializer function for a literal value - * + * * @param indent the indentation helper currently in scope * @param name the name of the serialization function * @param field the model field containing a literal to serialize @@ -658,13 +660,16 @@ function buildLiteralSerialize(indent: helpers.indentation, name: string, field: const toSerialize = `serializer.serialize_${serializeMethod}(${serializeValue})\n`; if (field.optional) { - content += `${indent.get()}${helpers.buildMatch(indent, `${fieldVar}.is_some()`, [{ - pattern: 'true', - body: (indent) => `${indent.get()}${toSerialize}`, - }, { - pattern: 'false', - body: (indent) => `${indent.get()}serializer.serialize_none()\n`, - }])}` + content += `${indent.get()}${helpers.buildMatch(indent, `${fieldVar}.is_some()`, [ + { + pattern: 'true', + body: (indent) => `${indent.get()}${toSerialize}`, + }, + { + pattern: 'false', + body: (indent) => `${indent.get()}serializer.serialize_none()\n`, + }, + ])}`; } else { content += `${indent.get()}${toSerialize}`; } @@ -675,7 +680,7 @@ function buildLiteralSerialize(indent: helpers.indentation, name: string, field: /** * constructs a serde deserialize function - * + * * @param indent the indentation helper currently in scope * @param type the type for which to build the helper * @param use the use statement builder currently in scope @@ -691,17 +696,18 @@ function buildDeserialize(indent: helpers.indentation, type: rust.Type, use: Use content += `${indent.get()}${helpers.buildMatch(indent, 'to_deserialize', [ { pattern: 'Some(to_deserialize)', - body: (indent) => recursiveBuildDeserializeBody(indent, use, { - caller: 'start', - type: type, - srcVar: 'to_deserialize', - destVar: new VarStack('decoded'), - }), + body: (indent) => + recursiveBuildDeserializeBody(indent, use, { + caller: 'start', + type: type, + srcVar: 'to_deserialize', + destVar: new VarStack('decoded'), + }), }, { pattern: 'None', body: (indent) => `${indent.get()}Ok(${type.kind === 'option' ? 'None' : `<${getSerDeTypeDeclaration(type, 'result')}>::default()`})\n`, - } + }, ])}\n`; content += `${indent.pop().get()}}\n`; return content; @@ -709,7 +715,7 @@ function buildDeserialize(indent: helpers.indentation, type: rust.Type, use: Use /** * constructs a serde serialize function - * + * * @param indent the indentation helper currently in scope * @param type the type for which to build the helper * @param use the use statement builder currently in scope @@ -752,7 +758,7 @@ class VarStack { /** * returns the var name at the top of the stack - * + * * @returns the var name */ get(): string { @@ -762,7 +768,7 @@ class VarStack { /** * returns the previous var name on the stack. * if push() has not been called, an error is thrown. - * + * * @returns the previous var name */ prev(): string { @@ -774,7 +780,7 @@ class VarStack { /** * adds the next var to the top of the stack - * + * * @returns this with updated stack state */ push(): VarStack { @@ -809,7 +815,7 @@ interface stateCtx { type: rust.Type; /** the var name of the content currently being processed */ - srcVar: string + srcVar: string; /** the stack of destination var names */ destVar: VarStack; @@ -817,7 +823,7 @@ interface stateCtx { /** * recursive state machine to construct the body of the deserialize function. - * + * * @param indent the indentation helper currently in scope * @param use the use statement builder currently in scope * @param ctx the current context of the state machine @@ -827,7 +833,7 @@ function recursiveBuildDeserializeBody(indent: helpers.indentation, use: Use, ct /** * adds the var in val to the collection, or does nothing * depending on the value of caller. - * + * * when valAsDefault is true, the value in val is returned. * else the empty string is returned. */ @@ -910,7 +916,7 @@ function recursiveBuildDeserializeBody(indent: helpers.indentation, use: Use, ct /** * recursive state machine to construct the body of the serialize function. - * + * * @param indent the indentation helper currently in scope * @param use the use statement builder currently in scope * @param ctx the current context of the state machine @@ -919,7 +925,7 @@ function recursiveBuildDeserializeBody(indent: helpers.indentation, use: Use, ct function recursiveBuildSerializeBody(indent: helpers.indentation, use: Use, ctx: stateCtx): string { /** inserts the var in val into the current HashMap */ const hashMapInsert = function (val: string): string { - return `${indent.get()}${ctx.destVar.prev()}.insert(kv.0, ${val});\n` + return `${indent.get()}${ctx.destVar.prev()}.insert(kv.0, ${val});\n`; }; let content = ''; @@ -983,19 +989,21 @@ function recursiveBuildSerializeBody(indent: helpers.indentation, use: Use, ctx: break; } case 'option': { - content = indent.get() + helpers.buildIfBlock(indent, { - condition: `let Some(${ctx.srcVar}) = ${ctx.srcVar}`, - body: (indent) => { - let body = recursiveBuildSerializeBody(indent, use, { - caller: 'option', - type: (ctx.type).type, - srcVar: ctx.srcVar, - destVar: ctx.destVar, - }); - body += `${indent.get()}<${getSerDeTypeDeclaration(ctx.type, 'serialize')}>::serialize(&Some(${ctx.destVar.get()}), serializer)\n`; - return body; - } - }); + content = + indent.get() + + helpers.buildIfBlock(indent, { + condition: `let Some(${ctx.srcVar}) = ${ctx.srcVar}`, + body: (indent) => { + let body = recursiveBuildSerializeBody(indent, use, { + caller: 'option', + type: (ctx.type).type, + srcVar: ctx.srcVar, + destVar: ctx.destVar, + }); + body += `${indent.get()}<${getSerDeTypeDeclaration(ctx.type, 'serialize')}>::serialize(&Some(${ctx.destVar.get()}), serializer)\n`; + return body; + }, + }); content += ` else {\n${indent.push().get()}serializer.serialize_none()\n${indent.pop().get()}}\n`; break; } @@ -1040,13 +1048,13 @@ function recursiveBuildSerializeBody(indent: helpers.indentation, use: Use, ctx: * the target type declarations in the serde helpers. * the type declarations are slightly different depending on the usage * context and the underlying generic type. - * + * * @param type is the Rust type for which to emit the declaration * @param usage defines the context in which the type is being used. * serialize - type is used in the serialize function * deserialize - type is used in the deserialize function * result - type is used as the result type in the deserialize function - * @returns + * @returns */ function getSerDeTypeDeclaration(type: rust.Type, usage: 'serialize' | 'deserialize' | 'result'): string { switch (type.kind) { @@ -1067,7 +1075,7 @@ function getSerDeTypeDeclaration(type: rust.Type, usage: 'serialize' | 'deserial /** * returns true if the provided type should be encoded as a string. - * + * * @param type the type for which to check the encoding * @returns true if string encoding is required */ diff --git a/packages/typespec-rust/src/codegen/use.ts b/packages/typespec-rust/src/codegen/use.ts index e0957a169..e9b5b2ed1 100644 --- a/packages/typespec-rust/src/codegen/use.ts +++ b/packages/typespec-rust/src/codegen/use.ts @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ import * as helpers from './helpers.js'; import { CodegenError } from './errors.js'; @@ -14,7 +14,7 @@ export class Use { /** * instantiates a new instance of the Use type - * + * * @param scope indicates a scope in which use statements are constructed. * this is only applicable when constructing the path to generated types. * clients - we're in generated/clients @@ -31,13 +31,13 @@ export class Use { * e.g. ('azure_core', 'Context') or ('super::models', 'FooType') * NOTE: the leaf value MUST be a symbol within a module and not * just a module. - * + * * @param module a module name * @param type one or more functions/types within the provided module */ add(module: string, ...types: Array): void { if (types.length === 0) { - throw new CodegenError('InternalError', 'types can\'t be empty'); + throw new CodegenError('InternalError', "types can't be empty"); } for (const type of types) { @@ -47,7 +47,7 @@ export class Use { chunks.push(...type.split('::')); // each tree starts at the root of the fully qualified path - let tree = this.trees.find(v => v.root.name === chunks[0]); + let tree = this.trees.find((v) => v.root.name === chunks[0]); if (!tree) { tree = new useTree(chunks[0]); this.trees.push(tree); @@ -60,7 +60,7 @@ export class Use { /** * adds the specified type if not already in the list - * + * * @param type the Rust type to add */ addForType(type: rust.Client | rust.Payload | rust.ResponseHeadersTrait | rust.Type): void { @@ -205,7 +205,7 @@ export class Use { /** * a tree of use statements - * + * * the tree starts at a root (e.g. azure_core) and * each node has one or more children. */ @@ -234,6 +234,6 @@ class useNode { readonly children: Array; constructor(name: string) { this.name = name; - this.children = new Array; + this.children = new Array(); } } diff --git a/packages/typespec-rust/src/codemodel/client.ts b/packages/typespec-rust/src/codemodel/client.ts index 89f7a8fbd..7aa75999d 100644 --- a/packages/typespec-rust/src/codemodel/client.ts +++ b/packages/typespec-rust/src/codemodel/client.ts @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ import * as method from './method.js'; import * as types from './types.js'; @@ -181,25 +181,25 @@ export type ParameterStyle = // For scalar values, it works the same regardless of 'explode'. // For arrays, "{placeholder}" becomes "v,a,l,u,e,s", regardless of 'explode'. // For hashmaps, it is "k1,v1,k2,v2" when 'explode' is false, "k1=v1,k2=v2" when true. - 'simple' | + | 'simple' /** Expansion of value into path components via '/': "{placeholder}" becomes "/value" */ // For scalar values, it works the same regardless of 'explode'. // For arrays, "{placeholder}" becomes "/v,a,l,u,e,s" when 'explode' is false, "/v/a/l/u/e/s" when true. // For hashmaps, it is "/k1,v1,k2,v2" when 'explode' is false, "/k1=v1/k2=v2" when true. - 'path' | + | 'path' /** Expansion of value into a label via '.': "{placeholder}" becomes ".value" */ // For scalar values, it works the same regardless of 'explode'. // For arrays, "{placeholder}" becomes ".v,a,l,u,e,s" when 'explode' is false, ".v.a.l.u.e.s" when true. // For hashmaps, it is ".k1,v1,k2,v2" when 'explode' is false, ".k1=v1.k2=v2" when true. - 'label' | + | 'label' /** Semicolon separated, 'name=value' form: "{placeholder}" becomes ";param=value" */ // For scalar values, it works the same regardless of 'explode'. // For arrays, "{placeholder}" becomes ";param=v,a,l,u,e,s" when 'explode' is false, ";param=v;param=a;param=l;param=u;param=e;param=s" when true. // For hashmaps, it is ";param=k1,v1,k2,v2" when 'explode' is false, ";k1=v1;k2=v2" when true. - 'matrix'; + | 'matrix'; /** CollectionFormat indicates how a collection is formatted on the wire */ export type CollectionFormat = 'csv' | 'ssv' | 'tsv' | 'pipes'; @@ -211,7 +211,18 @@ export type ExtendedCollectionFormat = CollectionFormat | 'multi'; export type ParameterLocation = 'client' | 'method'; /** MethodParameter defines the possible method parameter types */ -export type MethodParameter = BodyParameter | HeaderCollectionParameter | HeaderHashMapParameter | HeaderScalarParameter | PartialBodyParameter | PathCollectionParameter | PathHashMapParameter | PathScalarParameter | QueryCollectionParameter | QueryHashMapParameter | QueryScalarParameter; +export type MethodParameter = + | BodyParameter + | HeaderCollectionParameter + | HeaderHashMapParameter + | HeaderScalarParameter + | PartialBodyParameter + | PathCollectionParameter + | PathHashMapParameter + | PathScalarParameter + | QueryCollectionParameter + | QueryHashMapParameter + | QueryScalarParameter; /** BodyParameter is a param that's passed via the HTTP request body */ export interface BodyParameter extends HTTPParameterBase { @@ -266,7 +277,7 @@ export interface HeaderScalarParameter extends HTTPParameterBase { type: HeaderScalarParameterType; /** - * indicates this is an API version parameter + * indicates this is an API version parameter * the default value is false. */ isApiVersion: boolean; @@ -411,7 +422,7 @@ export interface QueryScalarParameter extends HTTPParameterBase { encoded: boolean; /** - * indicates this is an API version parameter + * indicates this is an API version parameter * the default value is false. */ isApiVersion: boolean; @@ -619,7 +630,7 @@ export class Constructor implements Constructor { } } -export class ClientEndpointParameter extends ClientParameterBase implements ClientEndpointParameter{ +export class ClientEndpointParameter extends ClientParameterBase implements ClientEndpointParameter { constructor(name: string, type: types.Type, optional: boolean, segment: string) { super(name, type, optional); this.kind = 'clientEndpoint'; @@ -693,7 +704,16 @@ export class PartialBodyParameter extends HTTPParameterBase implements PartialBo } export class PathCollectionParameter extends HTTPParameterBase implements PathCollectionParameter { - constructor(name: string, segment: string, location: ParameterLocation, optional: boolean, type: PathCollectionParameterType, encoded: boolean, style: ParameterStyle, explode: boolean) { + constructor( + name: string, + segment: string, + location: ParameterLocation, + optional: boolean, + type: PathCollectionParameterType, + encoded: boolean, + style: ParameterStyle, + explode: boolean, + ) { super(name, location, optional, type); this.kind = 'pathCollection'; this.segment = segment; @@ -784,7 +804,7 @@ export class ResponseHeadersTrait implements ResponseHeadersTrait { } } -export class SupplementalEndpoint implements SupplementalEndpoint{ +export class SupplementalEndpoint implements SupplementalEndpoint { constructor(path: string) { this.path = path; this.parameters = new Array(); diff --git a/packages/typespec-rust/src/codemodel/crate.ts b/packages/typespec-rust/src/codemodel/crate.ts index 799fc5bf6..aedcf86cc 100644 --- a/packages/typespec-rust/src/codemodel/crate.ts +++ b/packages/typespec-rust/src/codemodel/crate.ts @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ import * as client from './client.js'; import * as types from './types.js'; @@ -71,7 +71,7 @@ export class Crate implements Crate { for (const dep of this.dependencies) { if (dep.name === dependency.name) { // merge in any features - dep.features = dep.features.concat(dependency.features.filter(item => !dep.features.includes(item))); + dep.features = dep.features.concat(dependency.features.filter((item) => !dep.features.includes(item))); return; } } @@ -80,34 +80,54 @@ export class Crate implements Crate { /** lexicographically sorts all content */ sortContent(): void { - const sortAscending = function(a: string, b: string): number { + const sortAscending = function (a: string, b: string): number { return a < b ? -1 : a > b ? 1 : 0; }; - this.dependencies.sort((a: CrateDependency, b: CrateDependency) => { return sortAscending(a.name, b.name); }); - this.enums.sort((a: types.Enum, b: types.Enum) => { return sortAscending(a.name, b.name); }); + this.dependencies.sort((a: CrateDependency, b: CrateDependency) => { + return sortAscending(a.name, b.name); + }); + this.enums.sort((a: types.Enum, b: types.Enum) => { + return sortAscending(a.name, b.name); + }); for (const rustEnum of this.enums) { - rustEnum.values.sort((a: types.EnumValue, b: types.EnumValue) => { return sortAscending(a.name, b.name); }); + rustEnum.values.sort((a: types.EnumValue, b: types.EnumValue) => { + return sortAscending(a.name, b.name); + }); } - this.models.sort((a: types.MarkerType | types.Model, b: types.MarkerType | types.Model) => { return sortAscending(a.name, b.name); }); + this.models.sort((a: types.MarkerType | types.Model, b: types.MarkerType | types.Model) => { + return sortAscending(a.name, b.name); + }); for (const model of this.models) { if (model.kind === 'marker') { continue; } - model.fields.sort((a: types.ModelField, b: types.ModelField) => { return sortAscending(a.name, b.name); }); + model.fields.sort((a: types.ModelField, b: types.ModelField) => { + return sortAscending(a.name, b.name); + }); } - this.clients.sort((a: client.Client, b: client.Client) => { return sortAscending(a.name, b.name); }); + this.clients.sort((a: client.Client, b: client.Client) => { + return sortAscending(a.name, b.name); + }); for (const client of this.clients) { - client.fields.sort((a: types.StructField, b: types.StructField) => { return sortAscending(a.name, b.name); }); - client.methods.sort((a: client.MethodType, b: client.MethodType) => { return sortAscending(a.name, b.name); }); + client.fields.sort((a: types.StructField, b: types.StructField) => { + return sortAscending(a.name, b.name); + }); + client.methods.sort((a: client.MethodType, b: client.MethodType) => { + return sortAscending(a.name, b.name); + }); if (client.constructable) { - client.constructable.options.type.fields.sort((a: types.StructField, b: types.StructField) => { return sortAscending(a.name, b.name); }); + client.constructable.options.type.fields.sort((a: types.StructField, b: types.StructField) => { + return sortAscending(a.name, b.name); + }); } for (const method of client.methods) { if (method.kind === 'clientaccessor') { continue; } - method.options.type.fields.sort((a: types.StructField, b: types.StructField) => { return sortAscending(a.name, b.name); }); + method.options.type.fields.sort((a: types.StructField, b: types.StructField) => { + return sortAscending(a.name, b.name); + }); method.responseHeaders?.headers.sort((a: client.ResponseHeader, b: client.ResponseHeader) => sortAscending(a.header, b.header)); } } diff --git a/packages/typespec-rust/src/codemodel/index.ts b/packages/typespec-rust/src/codemodel/index.ts index 9cbe7819a..677a21d40 100644 --- a/packages/typespec-rust/src/codemodel/index.ts +++ b/packages/typespec-rust/src/codemodel/index.ts @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ export * from './client.js'; export * from './crate.js'; diff --git a/packages/typespec-rust/src/codemodel/method.ts b/packages/typespec-rust/src/codemodel/method.ts index b73b51026..ebd89d1a8 100644 --- a/packages/typespec-rust/src/codemodel/method.ts +++ b/packages/typespec-rust/src/codemodel/method.ts @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ import * as types from './types.js'; diff --git a/packages/typespec-rust/src/codemodel/types.ts b/packages/typespec-rust/src/codemodel/types.ts index 0e0b1f8c2..cbeef6ea4 100644 --- a/packages/typespec-rust/src/codemodel/types.ts +++ b/packages/typespec-rust/src/codemodel/types.ts @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ import { Crate, CrateDependency } from './crate.js'; @@ -15,10 +15,44 @@ export interface Docs { } /** SdkType defines types used in generated code but do not directly participate in serde */ -export type SdkType = Arc | Box | ExternalType | ImplTrait | MarkerType | Option | PageIterator | Pager | RawResponse | RequestContent | Response | Result | Struct | TokenCredential | Unit; +export type SdkType = + | Arc + | Box + | ExternalType + | ImplTrait + | MarkerType + | Option + | PageIterator + | Pager + | RawResponse + | RequestContent + | Response + | Result + | Struct + | TokenCredential + | Unit; /** WireType defines types that go across the wire */ -export type WireType = Bytes | Decimal | EncodedBytes | Enum | EnumValue | Etag | HashMap | JsonValue | Literal | Model | OffsetDateTime | RefBase | SafeInt | Scalar | Slice | StringSlice | StringType | Url | Vector; +export type WireType = + | Bytes + | Decimal + | EncodedBytes + | Enum + | EnumValue + | Etag + | HashMap + | JsonValue + | Literal + | Model + | OffsetDateTime + | RefBase + | SafeInt + | Scalar + | Slice + | StringSlice + | StringType + | Url + | Vector; /** Type defines a type within the Rust type system */ export type Type = SdkType | WireType; @@ -450,7 +484,7 @@ export type XMLKind = 'attribute' | 'text' | 'unwrappedList'; /** * QualifiedType is a fully qualified type. - * + * * this is typically a type in the standard library that's not in the prelude set. */ export interface QualifiedType { @@ -477,13 +511,13 @@ export type Visibility = 'pub' | 'pubCrate'; /** * External is a qualified type defined in a different crate - * + * * the value in path will be used to determine the crate name */ interface External extends QualifiedType {} class External extends QualifiedType implements External { - constructor(crate: Crate, name: string, path: string, features = new Array) { + constructor(crate: Crate, name: string, path: string, features = new Array()) { super(name, path); let crateName = this.path; const pathSep = crateName.indexOf('::'); diff --git a/packages/typespec-rust/src/emitter.ts b/packages/typespec-rust/src/emitter.ts index f5c6640c8..408b6cb21 100644 --- a/packages/typespec-rust/src/emitter.ts +++ b/packages/typespec-rust/src/emitter.ts @@ -18,7 +18,7 @@ import 'source-map-support/register.js'; /** * entry point called by the tsp compiler - * + * * @param context the emit context */ export async function $onEmit(context: EmitContext) { @@ -71,7 +71,7 @@ export async function $onEmit(context: EmitContext) { target: error.target, format: { stack: error.stack ? truncateStack(error.stack, 'tcgcToCrate') : 'Stack trace unavailable\n', - } + }, }); } else if (error instanceof CodegenError) { reportDiagnostic(context.program, { @@ -79,7 +79,7 @@ export async function $onEmit(context: EmitContext) { target: NoTarget, format: { stack: error.stack ? truncateStack(error.stack, 'tcgcToCrate') : 'Stack trace unavailable\n', - } + }, }); } else { throw error; @@ -122,7 +122,7 @@ export async function $onEmit(context: EmitContext) { } /** - * + * * @param outDir the output directory provided by the tsp compiler * @param filename the name of the file to write. can contain sub-directories * @param content the contents of the file @@ -136,7 +136,7 @@ async function writeToGeneratedDir(outDir: string, filename: string, content: st /** * drop frames after the specified frame. - * + * * @param stack the stack to truncate * @returns the truncated stack */ diff --git a/packages/typespec-rust/src/lib.ts b/packages/typespec-rust/src/lib.ts index 6c638204d..ea26e6d0c 100644 --- a/packages/typespec-rust/src/lib.ts +++ b/packages/typespec-rust/src/lib.ts @@ -23,68 +23,66 @@ const EmitterOptionsSchema: JSONSchemaType = { type: 'object', additionalProperties: true, properties: { - 'crate-name': { - type: 'string', + 'crate-name': { + type: 'string', nullable: false, - description: 'The name of the generated Rust crate' + description: 'The name of the generated Rust crate', }, - 'crate-version': { - type: 'string', + 'crate-version': { + type: 'string', nullable: false, - description: 'The version of the generated Rust crate' + description: 'The version of the generated Rust crate', }, - 'overwrite-cargo-toml': { - type: 'boolean', - nullable: false, + 'overwrite-cargo-toml': { + type: 'boolean', + nullable: false, default: false, - description: 'Whether to overwrite an existing Cargo.toml file. Defaults to false' + description: 'Whether to overwrite an existing Cargo.toml file. Defaults to false', }, - 'overwrite-lib-rs': { - type: 'boolean', - nullable: false, + 'overwrite-lib-rs': { + type: 'boolean', + nullable: false, default: false, - description: 'Whether to overwrite an existing lib.rs file. Defaults to false' + description: 'Whether to overwrite an existing lib.rs file. Defaults to false', }, - 'temp-omit-doc-links': { - type: 'boolean', - nullable: false, + 'temp-omit-doc-links': { + type: 'boolean', + nullable: false, default: false, - description: 'Whether to omit documentation links in generated code. Defaults to false' + description: 'Whether to omit documentation links in generated code. Defaults to false', }, }, - required: [ - 'crate-name', - 'crate-version', - ], + required: ['crate-name', 'crate-version'], }; const libDef = { name: '@azure-tools/typespec-rust', diagnostics: { - 'InternalError': { + InternalError: { severity: 'error', messages: { - default: paramMessage`The emitter encountered an internal error during preprocessing. Please open an issue at https://github.com/Azure/typespec-rust/issues and include the complete error message.\n${'stack'}` - } + default: paramMessage`The emitter encountered an internal error during preprocessing. Please open an issue at https://github.com/Azure/typespec-rust/issues and include the complete error message.\n${'stack'}`, + }, }, - 'InvalidArgument': { + InvalidArgument: { severity: 'error', messages: { - default: 'Invalid arguments were passed to the emitter.' - } + default: 'Invalid arguments were passed to the emitter.', + }, }, - 'NameCollision': { + NameCollision: { severity: 'error', messages: { - default: 'The emitter automatically renamed one or more items which resulted in a name collision. Please update the client.tsp to rename the type(s) to avoid the collision.' - } + default: + 'The emitter automatically renamed one or more items which resulted in a name collision. Please update the client.tsp to rename the type(s) to avoid the collision.', + }, }, - 'UnsupportedTsp': { + UnsupportedTsp: { severity: 'error', messages: { - default: paramMessage`The emitter encountered a TypeSpec definition that is currently not supported.\n${'stack'}` - } - } + default: paramMessage`The emitter encountered a TypeSpec definition that is currently not supported.\n${'stack'}`, + }, + }, }, emitter: { options: >EmitterOptionsSchema, diff --git a/packages/typespec-rust/src/shared/shared.ts b/packages/typespec-rust/src/shared/shared.ts index 2920d1ad1..ddb23791b 100644 --- a/packages/typespec-rust/src/shared/shared.ts +++ b/packages/typespec-rust/src/shared/shared.ts @@ -1,13 +1,13 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ import * as rust from '../codemodel/index.js'; /** * if type is an Option, returns the T, else returns type - * + * * @param type is the type to unwrap * @returns the wrapped type or the original type if it wasn't wrapped */ @@ -22,10 +22,10 @@ export function unwrapOption(type: rust.Type): rust.Type { * returns the object of type targetKind extracted from type or undefined. * if targetKind is wrapped within one or more types, their kind(s) must be specified in wrappedIn. * if the specified sequence of types (wrappedIn + targetKind) don't match, undefined is returned. - * + * * e.g. to obtain the underlying 'str' from a &str, the call would be made as follows. * const asStr = asTypeOf(objInstance, 'str', 'ref'); - * + * * @param type the object instance from which to extract targetKind * @param targetKind the target type's kind when performing the conversion. note that this value MUST match the kind for the specified generic type parameter * @param wrappedIn the kinds of any wrapper types that contain targetKind. can be empty if targetKind isn't wrapped. @@ -55,7 +55,7 @@ export function asTypeOf(type: rust.Type, targetKind: rust. * returns a wrapper type's inner type. * e.g. for a rust.Vector, return's the value of Vector.type. * if the type doesn't wrap another type, undefined is returned. - * + * * @param type the type to unwrap * @returns the inner type or undefined */ diff --git a/packages/typespec-rust/src/tcgcadapter/adapter.ts b/packages/typespec-rust/src/tcgcadapter/adapter.ts index b069be8bf..b53f18222 100644 --- a/packages/typespec-rust/src/tcgcadapter/adapter.ts +++ b/packages/typespec-rust/src/tcgcadapter/adapter.ts @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ // cspell: ignore responseheader subclients lropaging @@ -19,19 +19,19 @@ import * as rust from '../codemodel/index.js'; /** ErrorCode defines the types of adapter errors */ export type ErrorCode = /** the emitter encountered an internal error. this is always a bug in the emitter */ - 'InternalError' | + | 'InternalError' /** invalid arguments were passed to the emitter */ - 'InvalidArgument' | + | 'InvalidArgument' /** * renaming items resulted in one or more name collisions. * this will likely require an update to client.tsp to resolve. */ - 'NameCollision' | + | 'NameCollision' /** the emitter does not support the encountered TypeSpec construct */ - 'UnsupportedTsp'; + | 'UnsupportedTsp'; /** * AdapterError is thrown when the emitter fails to convert part of the tcgc code @@ -53,9 +53,9 @@ export class AdapterError extends Error { export class Adapter { /** * Creates an Adapter for the specified EmitContext. - * + * * @param context the compiler context from which to create the Adapter - * @returns + * @returns */ static async create(context: EmitContext): Promise { // @encodedName can be used in XML scenarios, it is effectively the @@ -107,7 +107,12 @@ export class Adapter { this.adaptClients(); // marker models don't require serde so exclude them from the check - if (this.crate.enums.length > 0 || values(this.crate.models).where(e => e.kind === 'model').any()) { + if ( + this.crate.enums.length > 0 || + values(this.crate.models) + .where((e) => e.kind === 'model') + .any() + ) { this.crate.addDependency(new rust.CrateDependency('serde')); } @@ -150,7 +155,7 @@ export class Adapter { return { summary: summary, description: doc, - } + }; } /** converts all tcgc types to their Rust type equivalent */ @@ -181,7 +186,7 @@ export class Adapter { /** * converts a tcgc enum to a Rust enum - * + * * @param sdkEnum the tcgc enum to convert * @returns a Rust enum */ @@ -208,7 +213,7 @@ export class Adapter { /** * converts a tcgc enumvalue to a Rust enum value. * this is typically used when a literal enum value is specified. - * + * * @param sdkEnumValue the tcgc enumvalue to convert * @returns a Rust enum value */ @@ -225,7 +230,7 @@ export class Adapter { /** * converts a tcgc model to a Rust model - * + * * @param model the tcgc model to convert * @param stack is a stack of model type names used to detect recursive type definitions * @returns a Rust model @@ -269,7 +274,11 @@ export class Adapter { let parent = model.baseModel; while (parent) { for (const parentProp of parent.properties) { - const exists = values(allProps).where(p => { return p.name === parentProp.name; }).first(); + const exists = values(allProps) + .where((p) => { + return p.name === parentProp.name; + }) + .first(); if (exists) { // don't add the duplicate. the TS compiler has better enforcement than OpenAPI // to ensure that duplicate fields with different types aren't added. @@ -307,7 +316,7 @@ export class Adapter { /** * converts a tcgc model property to a model field - * + * * @param property the tcgc model property to convert * @param modelVisibility the visibility of the model that contains the property * @param stack is a stack of model type names used to detect recursive type definitions @@ -327,7 +336,13 @@ export class Adapter { fieldType = new rust.Option(fieldType.kind === 'box' ? fieldType : this.typeToWireType(fieldType)); } - const modelField = new rust.ModelField(naming.getEscapedReservedName(snakeCaseName(property.name), 'prop'), property.serializedName, modelVisibility, fieldType, property.optional); + const modelField = new rust.ModelField( + naming.getEscapedReservedName(snakeCaseName(property.name), 'prop'), + property.serializedName, + modelVisibility, + fieldType, + property.optional, + ); modelField.docs = this.adaptDocs(property.summary, property.doc); // if this is a literal, add a doc comment explaining its behavior @@ -359,7 +374,7 @@ export class Adapter { /** * converts a tcgc type to a Rust type - * + * * @param type the tcgc type to convert * @param stack is a stack of model type names used to detect recursive type definitions * @returns the adapted Rust type @@ -612,7 +627,7 @@ export class Adapter { stringType = new rust.StringType(); this.types.set(typeKey, stringType); return stringType; - }; + } /** returns the Rust unit type */ private getUnitType(): rust.Unit { @@ -640,7 +655,7 @@ export class Adapter { /** * converts a tcgc constant to a Rust literal - * + * * @param constType the constant to convert * @returns a Rust literal */ @@ -694,7 +709,7 @@ export class Adapter { * formats input as a doc link. * e.g. [`${id}`](${link}) * if doc links are disabled, id is returned - * + * * @param id the ID of the doc link * @param link the target of the doc link * @returns the doc link or id @@ -709,7 +724,7 @@ export class Adapter { /** * recursively converts a client and its methods. * this simplifies the case for hierarchical clients. - * + * * @param client the tcgc client to recursively convert * @param parent contains the parent Rust client when converting a child client * @returns a Rust client @@ -954,7 +969,7 @@ export class Adapter { /** * creates a client constructor for the TokenCredential type. * the constructor is named new. - * + * * @param cred the OAuth2 credential to adapt * @returns a client constructor for TokenCredential */ @@ -979,7 +994,7 @@ export class Adapter { /** * converts a tcgc client parameter to a Rust client parameter - * + * * @param param the tcgc client parameter to convert * @param constructable contains client construction info. if the param is optional, it will go in the options type * @returns the Rust client parameter @@ -1030,13 +1045,18 @@ export class Adapter { /** * converts a tcgc client accessor method to a Rust method - * + * * @param client the tcgc client that contains the accessor method * @param method the tcgc client accessor method to convert * @param rustClient the client to which the method belongs * @param subClient the sub-client type that the method returns */ - private adaptClientAccessor(parentClient: tcgc.SdkClientType, childClient: tcgc.SdkClientType, rustClient: rust.Client, subClient: rust.Client): void { + private adaptClientAccessor( + parentClient: tcgc.SdkClientType, + childClient: tcgc.SdkClientType, + rustClient: rust.Client, + subClient: rust.Client, + ): void { const clientAccessor = new rust.ClientAccessor(`get_${snakeCaseName(subClient.name)}`, rustClient, subClient); clientAccessor.docs.summary = `Returns a new instance of ${subClient.name}.`; for (const param of childClient.clientInitialization.parameters) { @@ -1062,7 +1082,7 @@ export class Adapter { /** * converts a tcgc method to a Rust method for the specified client - * + * * @param method the tcgc method to convert * @param rustClient the client to which the method belongs */ @@ -1141,11 +1161,15 @@ export class Adapter { // most params have a one-to-one mapping. however, for spread params, there will // be a many-to-one mapping. i.e. multiple params will map to the same underlying // operation param. each param corresponds to a field within the operation param. - const opParam = values(allOpParams).where((opParam: tcgc.SdkHttpParameter) => { - return values(opParam.correspondingMethodParams).where((methodParam: tcgc.SdkModelPropertyType) => { - return methodParam.name === param.name; - }).any(); - }).first(); + const opParam = values(allOpParams) + .where((opParam: tcgc.SdkHttpParameter) => { + return values(opParam.correspondingMethodParams) + .where((methodParam: tcgc.SdkModelPropertyType) => { + return methodParam.name === param.name; + }) + .any(); + }) + .first(); if (!opParam) { throw new AdapterError('InternalError', `didn't find operation parameter for method ${method.name} parameter ${param.name}`, param.__raw?.node); @@ -1180,7 +1204,9 @@ export class Adapter { // sent in the request. we want the field within the model for this param. // NOTE: if the param is optional then the field is optional, thus it's // already wrapped in an Option type. - const field = adaptedParam.type.content.type.fields.find(f => { return f.name === adaptedParam.name; }); + const field = adaptedParam.type.content.type.fields.find((f) => { + return f.name === adaptedParam.name; + }); if (!field) { throw new AdapterError('InternalError', `didn't find spread param field ${adaptedParam.name} in type ${adaptedParam.type.content.type.name}`); } @@ -1363,7 +1389,7 @@ export class Adapter { * adapts response headers into Rust response headers and provides * a mapping from the tcgc response header to the Rust equivalent. * if there are no headers to adapt, an empty map is returned. - * + * * @param responseHeaders the response headers to adapt (can be empty) * @returns the map of response headers */ @@ -1390,7 +1416,7 @@ export class Adapter { /** * creates a Rust ResponseHeadersTrait for the specified response headers. * if there are no response headers, undefined is returned. - * + * * @param client the client that contains the method * @param method the method for which to create the trait * @param responseHeaders the response headers array (can be empty) @@ -1404,7 +1430,7 @@ export class Adapter { /** * recursively builds a name from the specified type. * e.g. Vec would be VecFooModel etc. - * + * * @param type the type for which to build a name * @returns the name */ @@ -1462,13 +1488,17 @@ export class Adapter { /** * creates the pageable strategy based on the method definition - * + * * @param method the pageable method for which to create a strategy * @param paramsMap maps tcgc method params to Rust params (needed for continuation token strategy) * @param respHeadersMap maps tcgc response headers to Rust response headers (needed for continuation token strategy) * @returns the pageable strategy */ - private adaptPageableMethodStrategy(method: tcgc.SdkPagingServiceMethod, paramsMap: Map, respHeadersMap: Map): rust.PageableStrategyKind { + private adaptPageableMethodStrategy( + method: tcgc.SdkPagingServiceMethod, + paramsMap: Map, + respHeadersMap: Map, + ): rust.PageableStrategyKind { if (method.pagingMetadata.nextLinkOperation) { // TODO: https://github.com/Azure/autorest.rust/issues/103 throw new AdapterError('UnsupportedTsp', 'next page operation NYI', method.__raw?.node); @@ -1549,7 +1579,7 @@ export class Adapter { /** * converts a tcgc operation parameter into a Rust method parameter - * + * * @param param the tcgc operation parameter to convert * @returns a Rust method parameter */ @@ -1653,41 +1683,43 @@ export class Adapter { adaptedParam.isApiVersion = param.isApiVersionParam; } break; - case 'path': { - paramType = this.typeToWireType(paramType); - let style: rust.ParameterStyle = 'simple'; + case 'path': { - const tspStyleString = (param.style as string); - if (!['simple', 'path', 'label', 'matrix'].includes(tspStyleString)) { - throw new AdapterError('InternalError', `unsupported style ${tspStyleString} for parameter ${param.serializedName}`, param.__raw?.node); - } else { - style = tspStyleString as rust.ParameterStyle; + paramType = this.typeToWireType(paramType); + let style: rust.ParameterStyle = 'simple'; + { + const tspStyleString = param.style as string; + if (!['simple', 'path', 'label', 'matrix'].includes(tspStyleString)) { + throw new AdapterError('InternalError', `unsupported style ${tspStyleString} for parameter ${param.serializedName}`, param.__raw?.node); + } else { + style = tspStyleString as rust.ParameterStyle; + } } - } - if (isRefSlice(paramType)) { - adaptedParam = new rust.PathCollectionParameter(paramName, param.serializedName, paramLoc, param.optional, paramType, param.allowReserved, style, param.explode); - } else if (paramType.kind === 'hashmap') { - adaptedParam = new rust.PathHashMapParameter(paramName, param.serializedName, paramLoc, param.optional, paramType, param.allowReserved, style, param.explode); - } else { - switch (paramType.kind) { - case 'jsonValue': - case 'model': - case 'slice': - case 'str': - case 'Vec': - throw new AdapterError('InternalError', `unexpected kind ${paramType.kind} for scalar path ${param.serializedName}`, param.__raw?.node); - } + if (isRefSlice(paramType)) { + adaptedParam = new rust.PathCollectionParameter(paramName, param.serializedName, paramLoc, param.optional, paramType, param.allowReserved, style, param.explode); + } else if (paramType.kind === 'hashmap') { + adaptedParam = new rust.PathHashMapParameter(paramName, param.serializedName, paramLoc, param.optional, paramType, param.allowReserved, style, param.explode); + } else { + switch (paramType.kind) { + case 'jsonValue': + case 'model': + case 'slice': + case 'str': + case 'Vec': + throw new AdapterError('InternalError', `unexpected kind ${paramType.kind} for scalar path ${param.serializedName}`, param.__raw?.node); + } - adaptedParam = new rust.PathScalarParameter(paramName, param.serializedName, paramLoc, param.optional, paramType, param.allowReserved, style); + adaptedParam = new rust.PathScalarParameter(paramName, param.serializedName, paramLoc, param.optional, paramType, param.allowReserved, style); + } } - } break; + break; case 'query': paramType = this.typeToWireType(paramType); if (paramType.kind === 'Vec' || isRefSlice(paramType)) { let format: rust.ExtendedCollectionFormat = param.explode ? 'multi' : 'csv'; if (param.collectionFormat) { - format = param.collectionFormat === 'simple' ? 'csv' : (param.collectionFormat === 'form' ? 'multi' : param.collectionFormat); + format = param.collectionFormat === 'simple' ? 'csv' : param.collectionFormat === 'form' ? 'multi' : param.collectionFormat; } // TODO: hard-coded encoding setting, https://github.com/Azure/typespec-azure/issues/1314 adaptedParam = new rust.QueryCollectionParameter(paramName, param.serializedName, paramLoc, param.optional, paramType, true, format); @@ -1723,7 +1755,7 @@ export class Adapter { * if no such transformation is necessary, undefined is returned. * e.g. a String param that doesn't need to be owned will be * returned as a &str. - * + * * @param type the param type to be updated * @param kind the kind of param * @returns the updated param type or undefined @@ -1775,7 +1807,7 @@ export class Adapter { /** * narrows a rust.Type to a rust.WireType. * if type isn't a rust.WireType, an error is thrown. - * + * * @param type the type to narrow * @returns the narrowed type */ @@ -1808,7 +1840,7 @@ export class Adapter { /** * converts a tcgc spread parameter into a Rust partial body parameter. - * + * * @param param the tcgc method parameter to convert * @param format the wire format for the underlying body type * @param opParamType the tcgc model to which the spread parameter belongs @@ -1836,13 +1868,20 @@ export class Adapter { const paramName = naming.getEscapedReservedName(snakeCaseName(param.name), 'param'); const paramLoc: rust.ParameterLocation = 'method'; - const adaptedParam = new rust.PartialBodyParameter(paramName, paramLoc, param.optional, serializedName, this.getType(param.type), new rust.RequestContent(this.crate, new rust.Payload(payloadType, format))); + const adaptedParam = new rust.PartialBodyParameter( + paramName, + paramLoc, + param.optional, + serializedName, + this.getType(param.type), + new rust.RequestContent(this.crate, new rust.Payload(payloadType, format)), + ); return adaptedParam; } /** * converts a Content-Type header value into a payload format - * + * * @param contentType the value of the Content-Type header * @returns a payload format */ @@ -1861,7 +1900,7 @@ export class Adapter { /** * converts an accept header value into a response format - * + * * @param accept the value of the Content-Type header * @returns a response format */ @@ -1895,10 +1934,10 @@ type tcgcScalarKind = 'boolean' | 'float' | 'float32' | 'float64' | 'int16' | 'i * transforms Etag etc to all lower case. * this is to prevent inadvertently snake-casing * Etag to e_tag. - * + * * if name isn't some variant of Etag the * original value is returned. - * + * * @param name the name to transform * @returns etag or the original value */ @@ -1910,7 +1949,7 @@ function fixETagName(name: string): string { * removes any illegal characters from the provided name. * note that characters _ and - are preserved so that the * proper snake-casing can be performed. - * + * * @param name the name to transform * @returns the transformed name or the original value */ @@ -1920,7 +1959,7 @@ function removeIllegalChars(name: string): string { /** * snake-cases the provided name - * + * * @param name the name to snake-case * @returns name in snake-case format */ @@ -1932,11 +1971,11 @@ function snakeCaseName(name: string): string { * recursively creates a map key from the specified type. * this is idempotent so providing the same type will create * the same key. - * + * * obj is recursively unwrapped, and each layer is used to construct * the key. e.g. if obj is a HashMap> this would * unwrap to hashmap-Vec-i32. - * + * * @param root the starting value for the key * @param obj the type for which to create the key * @returns a string containing the complete map key @@ -1973,7 +2012,7 @@ function recursiveKeyName(root: string, type: rust.WireType): string { /** * returns the XML-specific name based on the provided decorators - * + * * @param decorators the decorators to enumerate * @returns the XML-specific name or undefined if there isn't one */ @@ -1999,7 +2038,7 @@ function getXMLName(decorators: Array): string | undefined { /** * returns the XML-specific kind for field based on the provided decorators - * + * * @param decorators the decorators to enumerate * @param field the Rust model field to which the kind will apply * @returns the XML-specific field kind or undefined if there isn't one diff --git a/packages/typespec-rust/src/tcgcadapter/helpers.ts b/packages/typespec-rust/src/tcgcadapter/helpers.ts index 325c5e6c7..8b98509e5 100644 --- a/packages/typespec-rust/src/tcgcadapter/helpers.ts +++ b/packages/typespec-rust/src/tcgcadapter/helpers.ts @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ import * as codegen from '@azure-tools/codegen'; import * as tcgc from '@azure-tools/typespec-client-generator-core'; @@ -11,7 +11,7 @@ import * as rust from '../codemodel/index.js'; /** * fixes up enum names to follow Rust conventions - * + * * @param enumValue the enum value type to fix up * @returns the fixed up name. can be the original value if no fix-up was required */ @@ -22,7 +22,7 @@ export function fixUpEnumValueName(enumValue: tcgc.SdkEnumValueType): string { /** * split out from fixUpEnumValueName for testing purposes. * don't call this directly, call fixUpEnumValueName instead. - * + * * @param name the enum value name * @param kind the enum value's underlying kind * @returns the fixed up name. can be the original value if no fix-up was required @@ -89,7 +89,7 @@ export function fixUpEnumValueNameWorker(name: string, kind: tcgc.SdkBuiltInKind /** * sorts client params in place so they're in the order, endpoint, [credential], other - * + * * @param params the client parameters to sort */ export function sortClientParameters(params: Array): void { @@ -109,7 +109,7 @@ const tds = new turndownService({ codeBlockStyle: 'fenced', fence: '```' }); * applies certain formatting to a doc string. * if the doc string doesn't require formatting * the original doc string is returned. - * + * * @param docs the doc string to format * @returns the original or formatted doc string */ diff --git a/packages/typespec-rust/src/tcgcadapter/naming.ts b/packages/typespec-rust/src/tcgcadapter/naming.ts index cd57ff944..a9a04a94c 100644 --- a/packages/typespec-rust/src/tcgcadapter/naming.ts +++ b/packages/typespec-rust/src/tcgcadapter/naming.ts @@ -1,12 +1,12 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ /** * if name is a reserved word, append the suffix and return the result, else return name. * the suffix indicates the context in which name appears - * + * * @param name the name to potentially fix up * @param suffix the context in which name appears * @returns the fixed up name. can be the original value if no fix-up was required @@ -19,17 +19,64 @@ export function getEscapedReservedName(name: string, suffix: 'fn' | 'param' | 'p } // https://doc.rust-lang.org/reference/keywords.html -const reservedWords = new Set( - [ - // strict keywords - 'as', 'async', 'await', 'break', 'const', 'continue', 'crate', 'dyn', 'else', 'enum', 'extern', 'false', 'fn', - 'for', 'if', 'impl', 'in', 'let', 'loop', 'match', 'mod', 'move', 'mut', 'pub', 'ref', 'return', 'self', - 'Self', 'static', 'struct', 'super', 'trait', 'true', 'type', 'unsafe', 'use', 'where', 'while', +const reservedWords = new Set([ + // strict keywords + 'as', + 'async', + 'await', + 'break', + 'const', + 'continue', + 'crate', + 'dyn', + 'else', + 'enum', + 'extern', + 'false', + 'fn', + 'for', + 'if', + 'impl', + 'in', + 'let', + 'loop', + 'match', + 'mod', + 'move', + 'mut', + 'pub', + 'ref', + 'return', + 'self', + 'Self', + 'static', + 'struct', + 'super', + 'trait', + 'true', + 'type', + 'unsafe', + 'use', + 'where', + 'while', - // reserved keywords - 'abstract', 'become', 'box', 'do', 'final', 'macro', 'override', 'priv', 'try', 'typeof', 'unsized', 'virtual', 'yield', + // reserved keywords + 'abstract', + 'become', + 'box', + 'do', + 'final', + 'macro', + 'override', + 'priv', + 'try', + 'typeof', + 'unsized', + 'virtual', + 'yield', - // weak keywords - 'macro_rules', 'union', '\'static', - ] -); + // weak keywords + 'macro_rules', + 'union', + "'static", +]); diff --git a/packages/typespec-rust/test/codegen.test.ts b/packages/typespec-rust/test/codegen.test.ts index 18a1f371c..4babe2245 100644 --- a/packages/typespec-rust/test/codegen.test.ts +++ b/packages/typespec-rust/test/codegen.test.ts @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ // cspell: ignore ifblock @@ -14,7 +14,8 @@ import { describe, it } from 'vitest'; describe('typespec-rust: codegen', () => { describe('generateCargoTomlFile', () => { it('default Cargo.toml file', () => { - const expected = '[package]\n' + + const expected = + '[package]\n' + 'name = "test_crate"\n' + 'version = "1.2.3"\n' + 'authors.workspace = true\n' + @@ -29,7 +30,8 @@ describe('typespec-rust: codegen', () => { }); it('default Cargo.toml file with dependencies', () => { - const expected = '[package]\n' + + const expected = + '[package]\n' + 'name = "test_crate"\n' + 'version = "1.2.3"\n' + 'authors.workspace = true\n' + @@ -75,12 +77,11 @@ describe('typespec-rust: codegen', () => { const indent = new helpers.indentation(0); const ifblock = helpers.buildIfBlock(indent, { condition: 'foo == bar', - body: (indent) => { return `${indent.get()}bing = bong;\n`; } + body: (indent) => { + return `${indent.get()}bing = bong;\n`; + }, }); - const expected = - 'if foo == bar {\n' + - ' bing = bong;\n' + - '}'; + const expected = 'if foo == bar {\n' + ' bing = bong;\n' + '}'; strictEqual(ifblock, expected); }); @@ -92,14 +93,16 @@ describe('typespec-rust: codegen', () => { body: (ind) => { return `${ind.get()}${helpers.buildIfBlock(ind, { condition: 'foo == bar', - body: (ind) => `${ind.get()}bing = bong;\n` + body: (ind) => `${ind.get()}bing = bong;\n`, })}\n`; - } + }, }, { pattern: 'None', - body: (ind) => { return `${ind.get()}the none branch;\n`; } - } + body: (ind) => { + return `${ind.get()}the none branch;\n`; + }, + }, ]); const expected = 'match cond {\n' + @@ -124,15 +127,17 @@ describe('typespec-rust: codegen', () => { body: (ind) => { return `${ind.get()}${helpers.buildIfBlock(ind, { condition: 'foo == bar', - body: (ind) => `${ind.get()}bing = bong;\n` + body: (ind) => `${ind.get()}bing = bong;\n`, })}\n`; - } + }, }, { pattern: 'None', returns: 'Returns2', - body: (ind) => { return `${ind.get()}the none branch;\n`; } - } + body: (ind) => { + return `${ind.get()}the none branch;\n`; + }, + }, ]); const expected = 'match cond {\n' + diff --git a/packages/typespec-rust/test/lib.test.ts b/packages/typespec-rust/test/lib.test.ts index 98b4caafa..d115222a6 100644 --- a/packages/typespec-rust/test/lib.test.ts +++ b/packages/typespec-rust/test/lib.test.ts @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ import { $lib } from '../src/lib.js'; import { describe, expect, it } from 'vitest'; @@ -22,12 +22,14 @@ interface EmitterOptionsSchema { // Type guard to check if schema is properly structured function isEmitterOptionsSchema(schema: unknown): schema is EmitterOptionsSchema { - return typeof schema === 'object' && - schema !== null && - 'properties' in schema && - typeof (schema as Record).properties === 'object' && - 'required' in schema && - Array.isArray((schema as Record).required); + return ( + typeof schema === 'object' && + schema !== null && + 'properties' in schema && + typeof (schema as Record).properties === 'object' && + 'required' in schema && + Array.isArray((schema as Record).required) + ); } // Type guard to check if property has description @@ -39,7 +41,7 @@ describe('typespec-rust: lib', () => { it('should have documentation for all emitter options', () => { const schema = $lib.emitter?.options; expect(schema).toBeDefined(); - + if (!isEmitterOptionsSchema(schema)) { throw new Error('Emitter options schema is not properly structured'); } @@ -59,7 +61,7 @@ describe('typespec-rust: lib', () => { const property = properties[optionName]; expect(property).toBeDefined(); expect(typeof property).toBe('object'); - + // Each property should have a description if (property) { expect(hasDescription(property)).toBe(true); @@ -73,7 +75,7 @@ describe('typespec-rust: lib', () => { it('should have required options marked correctly', () => { const schema = $lib.emitter?.options; expect(schema).toBeDefined(); - + if (!isEmitterOptionsSchema(schema)) { throw new Error('Emitter options schema is not properly structured'); } @@ -85,13 +87,13 @@ describe('typespec-rust: lib', () => { it('should have appropriate default values for optional options', () => { const schema = $lib.emitter?.options; expect(schema).toBeDefined(); - + if (!isEmitterOptionsSchema(schema)) { throw new Error('Emitter options schema is not properly structured'); } const properties = schema.properties; - + // Check that boolean options have default values expect(properties['overwrite-cargo-toml']).toHaveProperty('default', false); expect(properties['overwrite-lib-rs']).toHaveProperty('default', false); diff --git a/packages/typespec-rust/test/shared.test.ts b/packages/typespec-rust/test/shared.test.ts index 8fcd3463a..bafdd1f00 100644 --- a/packages/typespec-rust/test/shared.test.ts +++ b/packages/typespec-rust/test/shared.test.ts @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ import * as rust from '../src/codemodel/index.js'; import * as shared from '../src/shared/shared.js'; diff --git a/packages/typespec-rust/test/tcgcadapter.test.ts b/packages/typespec-rust/test/tcgcadapter.test.ts index 55a07c952..593a9e78b 100644 --- a/packages/typespec-rust/test/tcgcadapter.test.ts +++ b/packages/typespec-rust/test/tcgcadapter.test.ts @@ -1,7 +1,7 @@ /*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ // cspell: ignore tcgcadapter @@ -71,9 +71,18 @@ describe('typespec-rust: tcgcadapter', () => { strictEqual(helpers.formatDocs('skip the period https://contoso.com/some-link.'), 'skip the period .'); strictEqual(helpers.formatDocs('already angled '), 'already angled '); strictEqual(helpers.formatDocs('anchor to markdown. inline'), 'anchor [to markdown.](https://contoso.com/fake/link) inline'); - strictEqual(helpers.formatDocs('anchor to markdown. and https://contoso.com/some-link'), 'anchor [to markdown.](https://contoso.com/fake/link) and '); - strictEqual(helpers.formatDocs('https://contoso.com/some-link anchor to markdown.'), ' anchor [to markdown.](https://contoso.com/fake/link)'); - strictEqual(helpers.formatDocs('https://contoso.com/some-link-one https://contoso.com/some-link-two https://contoso.com/some-link-three'), ' '); + strictEqual( + helpers.formatDocs('anchor to markdown. and https://contoso.com/some-link'), + 'anchor [to markdown.](https://contoso.com/fake/link) and ', + ); + strictEqual( + helpers.formatDocs('https://contoso.com/some-link anchor to markdown.'), + ' anchor [to markdown.](https://contoso.com/fake/link)', + ); + strictEqual( + helpers.formatDocs('https://contoso.com/some-link-one https://contoso.com/some-link-two https://contoso.com/some-link-three'), + ' ', + ); }); }); }); diff --git a/packages/typespec-rust/vitest.config.ts b/packages/typespec-rust/vitest.config.ts index 66a614ab0..1d19d01e2 100644 --- a/packages/typespec-rust/vitest.config.ts +++ b/packages/typespec-rust/vitest.config.ts @@ -12,7 +12,7 @@ export default defineConfig({ isolate: false, coverage: { reporter: ['cobertura', 'json', 'text'], - reportsDirectory: './coverage/tmp' + reportsDirectory: './coverage/tmp', }, outputFile: { junit: './test-results.xml', diff --git a/scripts/check-all-formats.sh b/scripts/check-all-formats.sh new file mode 100755 index 000000000..3f906bf0a --- /dev/null +++ b/scripts/check-all-formats.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Master script to check all code formatting +set -e + +echo "=== Running comprehensive format checks ===" +echo + +# Check TypeScript/JavaScript with ESLint +echo "1. Checking TypeScript/JavaScript formatting with ESLint..." +cd packages/typespec-rust +npx eslint src test --max-warnings=0 +echo "✅ ESLint checks passed!" +echo + +# Go back to root +cd ../.. + +# Check Prettier formatting +echo "2. Checking Prettier formatting (Markdown, JSON, TypeScript)..." +prettier --check "**/*.{md,json,ts,js}" \ + --ignore-path .gitignore \ + --ignore-path .prettierignore +echo "✅ Prettier checks passed!" +echo + +# Check TypeSpec files +echo "3. Validating TypeSpec files..." +./scripts/check-typespec-format.sh +echo "✅ TypeSpec validation passed!" +echo + +# Check Rust formatting +echo "4. Checking Rust formatting with rustfmt..." +./scripts/check-rust-format.sh +echo "✅ Rust formatting checks passed!" +echo + +echo "=== All format checks completed successfully! ===" \ No newline at end of file diff --git a/scripts/check-rust-format.sh b/scripts/check-rust-format.sh new file mode 100755 index 000000000..659018943 --- /dev/null +++ b/scripts/check-rust-format.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Script to check Rust formatting, excluding generated files with edition issues +set -e + +echo "Checking Rust file formatting with rustfmt..." + +# Find all Rust source files, excluding generated test files with edition issues +rust_files=$(find . -name "*.rs" -type f \ + ! -path "./packages/typespec-rust/test/spector/*/src/generated/*" \ + ! -path "./target/*" \ + ! -path "./*/target/*") + +if [ -z "$rust_files" ]; then + echo "No Rust source files found to check." + exit 0 +fi + +# Check each file with rustfmt +echo "Found $(echo "$rust_files" | wc -l) Rust files to check..." +echo "$rust_files" | xargs rustfmt --check --edition 2021 + +echo "All Rust files are properly formatted!" \ No newline at end of file diff --git a/scripts/check-typespec-format.sh b/scripts/check-typespec-format.sh new file mode 100755 index 000000000..05973318a --- /dev/null +++ b/scripts/check-typespec-format.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Script to validate TypeSpec files +set -e + +echo "Validating TypeSpec (.tsp) files..." + +# Find all TypeSpec files +tsp_files=$(find . -name "*.tsp" -type f ! -path "./node_modules/*" ! -path "./*/node_modules/*") + +if [ -z "$tsp_files" ]; then + echo "No TypeSpec files found." + exit 0 +fi + +echo "Found $(echo "$tsp_files" | wc -l) TypeSpec files..." + +# For now, just check that files exist and are readable +# In the future, this could be extended to use TypeSpec compiler for validation +for file in $tsp_files; do + if [ ! -r "$file" ]; then + echo "Error: Cannot read TypeSpec file: $file" + exit 1 + fi +done + +echo "All TypeSpec files are readable and found!" +echo "Note: TypeSpec formatting validation with prettier is not yet supported." +echo "Consider using TypeSpec-specific formatting tools if available." \ No newline at end of file diff --git a/scripts/format-all.sh b/scripts/format-all.sh new file mode 100755 index 000000000..a9214d8cb --- /dev/null +++ b/scripts/format-all.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Master script to format all code +set -e + +echo "=== Running comprehensive code formatting ===" +echo + +# Format TypeScript/JavaScript with ESLint +echo "1. Formatting TypeScript/JavaScript with ESLint..." +cd packages/typespec-rust +npx eslint src test --fix --max-warnings=0 +echo "✅ ESLint formatting completed!" +echo + +# Go back to root +cd ../.. + +# Format with Prettier +echo "2. Formatting with Prettier (Markdown, JSON, TypeScript)..." +prettier --write "**/*.{md,json,ts,js}" \ + --ignore-path .gitignore \ + --ignore-path .prettierignore +echo "✅ Prettier formatting completed!" +echo + +# Note about TypeSpec files +echo "3. TypeSpec files..." +echo "ℹ️ TypeSpec files currently require manual formatting (no automated tool available)" +echo + +# Format Rust files +echo "4. Formatting Rust files with rustfmt..." +# Find all Rust source files, excluding generated test files with edition issues +rust_files=$(find . -name "*.rs" -type f \ + ! -path "./packages/typespec-rust/test/spector/*/src/generated/*" \ + ! -path "./target/*" \ + ! -path "./*/target/*") + +if [ -n "$rust_files" ]; then + echo "Formatting $(echo "$rust_files" | wc -l) Rust files..." + echo "$rust_files" | xargs rustfmt --edition 2021 + echo "✅ Rust formatting completed!" +else + echo "No Rust source files found to format." +fi +echo + +echo "=== All code formatting completed! ===" \ No newline at end of file