Split core HTTP functionality from `@typespec/rest` into `@typespec/http` (#1668)
* Decouple `autoRoute` from HTTP library routing implementation * Split HTTP functionality of `@typespec/rest` to `@typespec/http` * Lint and format * Add Rush change files * Update playground * Generate separate documentation for HTTP library * Minor cleanup and library references * Add back `isAutoRoute` accessor function * Add `OperationParameterOptions` to customize operation parameter logic * Update playground configuration to add `@typespec/http` import * Remove duplicate diagnostics in REST library * Update REST and HTTP README files * Add a changelog entry about `@routeReset` removal
This commit is contained in:
Родитель
d1b20c1238
Коммит
3e669c74c1
|
@ -100,7 +100,7 @@ Here is a very small TypeSpec example that uses the `@typespec/openapi3` library
|
|||
#### sample.tsp
|
||||
|
||||
```typespec
|
||||
import "@typespec/rest";
|
||||
import "@typespec/http";
|
||||
|
||||
using TypeSpec.Http;
|
||||
|
||||
|
@ -183,6 +183,7 @@ Example
|
|||
// Stable setup
|
||||
"dependencies": {
|
||||
"@typespec/compiler": "~0.30.0",
|
||||
"@typespec/http": "~0.14.0",
|
||||
"@typespec/rest": "~0.14.0",
|
||||
"@typespec/openapi": "~0.9.0",
|
||||
}
|
||||
|
@ -191,6 +192,7 @@ Example
|
|||
// In this example: compiler and openapi have changes but rest library has none
|
||||
"dependencies": {
|
||||
"@typespec/compiler": "~0.31.0-dev.5",
|
||||
"@typespec/http": "~0.14.0",
|
||||
"@typespec/rest": "~0.14.0", // No changes to @typespec/rest library so need to stay the latest.
|
||||
"@typespec/openapi": "~0.10.0-dev.2",
|
||||
}
|
||||
|
@ -203,6 +205,7 @@ Example
|
|||
| Core functionality | | | |
|
||||
| [@typespec/compiler][compiler_src] | [Changelog][compiler_chg] | [![](https://img.shields.io/npm/v/@typespec/compiler)](https://www.npmjs.com/package/@typespec/compiler) | ![](https://img.shields.io/npm/v/@typespec/compiler/next) |
|
||||
| TypeSpec Libraries | | | |
|
||||
| [@typespec/http][http_src] | [Changelog][http_chg] | [![](https://img.shields.io/npm/v/@typespec/http)](https://www.npmjs.com/package/@typespec/http) | ![](https://img.shields.io/npm/v/@typespec/http/next) |
|
||||
| [@typespec/rest][rest_src] | [Changelog][rest_chg] | [![](https://img.shields.io/npm/v/@typespec/rest)](https://www.npmjs.com/package/@typespec/rest) | ![](https://img.shields.io/npm/v/@typespec/rest/next) |
|
||||
| [@typespec/openapi][openapi_src] | [Changelog][openapi_chg] | [![](https://img.shields.io/npm/v/@typespec/openapi)](https://www.npmjs.com/package/@typespec/openapi) | ![](https://img.shields.io/npm/v/@typespec/openapi/next) |
|
||||
| [@typespec/openapi3][openapi3_src] | [Changelog][openapi3_chg] | [![](https://img.shields.io/npm/v/@typespec/openapi3)](https://www.npmjs.com/package/@typespec/openapi3) | ![](https://img.shields.io/npm/v/@typespec/openapi3/next) |
|
||||
|
@ -215,6 +218,8 @@ Example
|
|||
|
||||
[compiler_src]: packages/compiler
|
||||
[compiler_chg]: packages/compiler/CHANGELOG.md
|
||||
[http_src]: packages/http
|
||||
[http_chg]: packages/http/CHANGELOG.md
|
||||
[rest_src]: packages/rest
|
||||
[rest_chg]: packages/rest/CHANGELOG.md
|
||||
[openapi_src]: packages/openapi
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@typespec/http",
|
||||
"comment": "Move core HTTP functionality from `@typespec/rest` into a new `@typespec/http` library",
|
||||
"type": "none"
|
||||
}
|
||||
],
|
||||
"packageName": "@typespec/http"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@typespec/openapi",
|
||||
"comment": "Use new `@typespec/http` library",
|
||||
"type": "none"
|
||||
}
|
||||
],
|
||||
"packageName": "@typespec/openapi"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@typespec/openapi3",
|
||||
"comment": "Use new `@typespec/http` library",
|
||||
"type": "none"
|
||||
}
|
||||
],
|
||||
"packageName": "@typespec/openapi3"
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@typespec/rest",
|
||||
"comment": "Move core HTTP functionality from `@typespec/rest` into a new `@typespec/http` library",
|
||||
"type": "none"
|
||||
},
|
||||
{
|
||||
"packageName": "@typespec/rest",
|
||||
"comment": "**Breaking change:** The `@autoRoute` decorator can no longer be applied to namespaces",
|
||||
"type": "none"
|
||||
},
|
||||
{
|
||||
"packageName": "@typespec/rest",
|
||||
"comment": "**Breaking change:** The unimplemented `@routeReset` decorator has been removed",
|
||||
"type": "none"
|
||||
}
|
||||
],
|
||||
"packageName": "@typespec/rest"
|
||||
}
|
|
@ -3,7 +3,6 @@ lockfileVersion: 5.3
|
|||
specifiers:
|
||||
'@babel/code-frame': ~7.18.6
|
||||
'@babel/core': ^7.0.0
|
||||
'@typespec/compiler-v0.37': npm:@typespec/compiler@0.37.0
|
||||
'@docusaurus/core': ^2.2.0
|
||||
'@docusaurus/module-type-aliases': ^2.2.0
|
||||
'@docusaurus/preset-classic': ^2.2.0
|
||||
|
@ -21,12 +20,11 @@ specifiers:
|
|||
'@rollup/plugin-replace': ~4.0.0
|
||||
'@rollup/plugin-virtual': ~3.0.1
|
||||
'@rush-temp/bundler': file:./projects/bundler.tgz
|
||||
'@rush-temp/typespec-vs': file:./projects/typespec-vs.tgz
|
||||
'@rush-temp/typespec-vscode': file:./projects/typespec-vscode.tgz
|
||||
'@rush-temp/compiler': file:./projects/compiler.tgz
|
||||
'@rush-temp/eslint-config-typespec': file:./projects/eslint-config-typespec.tgz
|
||||
'@rush-temp/eslint-plugin': file:./projects/eslint-plugin.tgz
|
||||
'@rush-temp/html-program-viewer': file:./projects/html-program-viewer.tgz
|
||||
'@rush-temp/http': file:./projects/http.tgz
|
||||
'@rush-temp/internal-build-utils': file:./projects/internal-build-utils.tgz
|
||||
'@rush-temp/library-linter': file:./projects/library-linter.tgz
|
||||
'@rush-temp/lint': file:./projects/lint.tgz
|
||||
|
@ -40,6 +38,8 @@ specifiers:
|
|||
'@rush-temp/samples': file:./projects/samples.tgz
|
||||
'@rush-temp/spec': file:./projects/spec.tgz
|
||||
'@rush-temp/tmlanguage-generator': file:./projects/tmlanguage-generator.tgz
|
||||
'@rush-temp/typespec-vs': file:./projects/typespec-vs.tgz
|
||||
'@rush-temp/typespec-vscode': file:./projects/typespec-vscode.tgz
|
||||
'@rush-temp/versioning': file:./projects/versioning.tgz
|
||||
'@rush-temp/website': file:./projects/website.tgz
|
||||
'@rushstack/eslint-patch': '1.1.0 '
|
||||
|
@ -64,6 +64,7 @@ specifiers:
|
|||
'@typescript-eslint/eslint-plugin': ^5.30.7
|
||||
'@typescript-eslint/parser': ^5.30.7
|
||||
'@typescript-eslint/utils': ~5.26.0
|
||||
'@typespec/compiler-v0.37': npm:@cadl-lang/compiler@0.37.0
|
||||
'@vitejs/plugin-react': ~2.2.0
|
||||
'@vscode/vsce': ~2.15.0
|
||||
ajv: ~8.11.2
|
||||
|
@ -130,7 +131,6 @@ specifiers:
|
|||
dependencies:
|
||||
'@babel/code-frame': 7.18.6
|
||||
'@babel/core': 7.20.12
|
||||
'@typespec/compiler-v0.37': /@typespec/compiler/0.37.0
|
||||
'@docusaurus/core': 2.3.0_8d89849c7c13db1dc34255908e1757ca
|
||||
'@docusaurus/module-type-aliases': 2.3.0_react-dom@18.2.0+react@18.2.0
|
||||
'@docusaurus/preset-classic': 2.3.0_d6adfce1a9735f2773fdfdc805a804cb
|
||||
|
@ -148,12 +148,11 @@ dependencies:
|
|||
'@rollup/plugin-replace': 4.0.0_rollup@3.4.0
|
||||
'@rollup/plugin-virtual': 3.0.1_rollup@3.4.0
|
||||
'@rush-temp/bundler': file:projects/bundler.tgz
|
||||
'@rush-temp/typespec-vs': file:projects/typespec-vs.tgz
|
||||
'@rush-temp/typespec-vscode': file:projects/typespec-vscode.tgz
|
||||
'@rush-temp/compiler': file:projects/compiler.tgz
|
||||
'@rush-temp/eslint-config-typespec': file:projects/eslint-config-typespec.tgz_prettier@2.8.3
|
||||
'@rush-temp/eslint-plugin': file:projects/eslint-plugin.tgz
|
||||
'@rush-temp/html-program-viewer': file:projects/html-program-viewer.tgz
|
||||
'@rush-temp/http': file:projects/http.tgz
|
||||
'@rush-temp/internal-build-utils': file:projects/internal-build-utils.tgz
|
||||
'@rush-temp/library-linter': file:projects/library-linter.tgz
|
||||
'@rush-temp/lint': file:projects/lint.tgz
|
||||
|
@ -167,6 +166,8 @@ dependencies:
|
|||
'@rush-temp/samples': file:projects/samples.tgz
|
||||
'@rush-temp/spec': file:projects/spec.tgz
|
||||
'@rush-temp/tmlanguage-generator': file:projects/tmlanguage-generator.tgz
|
||||
'@rush-temp/typespec-vs': file:projects/typespec-vs.tgz
|
||||
'@rush-temp/typespec-vscode': file:projects/typespec-vscode.tgz
|
||||
'@rush-temp/versioning': file:projects/versioning.tgz
|
||||
'@rush-temp/website': file:projects/website.tgz_@types+react@18.0.27
|
||||
'@rushstack/eslint-patch': 1.1.0
|
||||
|
@ -191,6 +192,7 @@ dependencies:
|
|||
'@typescript-eslint/eslint-plugin': 5.49.0_67a61cd63cbad5b63527389b6b4cdf8f
|
||||
'@typescript-eslint/parser': 5.49.0_eslint@8.33.0+typescript@4.9.5
|
||||
'@typescript-eslint/utils': 5.26.0_eslint@8.33.0+typescript@4.9.5
|
||||
'@typespec/compiler-v0.37': /@cadl-lang/compiler/0.37.0
|
||||
'@vitejs/plugin-react': 2.2.0_vite@3.2.5
|
||||
'@vscode/vsce': 2.15.0
|
||||
ajv: 8.11.2
|
||||
|
@ -1772,7 +1774,7 @@ packages:
|
|||
resolution: {integrity: sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==}
|
||||
dev: false
|
||||
|
||||
/@typespec/compiler/0.37.0:
|
||||
/@cadl-lang/compiler/0.37.0:
|
||||
resolution: {integrity: sha512-jHMqPZmM4evQlu7oY9vj6PEM+f+OhnfPqAdwxALrU2gwsLcwYG1h8rkjX/iK2KfeewCbXuRT/hztOTo3pcbYWA==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
hasBin: true
|
||||
|
@ -15281,7 +15283,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/bundler.tgz:
|
||||
resolution: {integrity: sha512-sUO7vssAYoEY7pQrVpcJ0MEHIhN5Lrqlc79wmBLPHb3tuWGpHPGA909BIGNHNuneGLO68kA/gMlDGgBfv4cNgg==, tarball: file:projects/bundler.tgz}
|
||||
resolution: {integrity: sha512-2QTp6cW0nHCRYpyhBuY0LJxfQaXujdjaApLqkJHOJZFu5X++cSMFIDH7E8yTgCBBOTCes/my2erb7GMPuoUQHg==, tarball: file:projects/bundler.tgz}
|
||||
name: '@rush-temp/bundler'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
@ -15310,40 +15312,8 @@ packages:
|
|||
- terser
|
||||
dev: false
|
||||
|
||||
file:projects/typespec-vs.tgz:
|
||||
resolution: {integrity: sha512-i/10VOQOkEl9M0wDLMHSYuZlwiwnSM2gqt7xmTUcdEn5vkbufZGa0x9ZoLRhnC91DB4QYPb67i7IxYeemOKG6w==, tarball: file:projects/typespec-vs.tgz}
|
||||
name: '@rush-temp/typespec-vs'
|
||||
version: 0.0.0
|
||||
dev: false
|
||||
|
||||
file:projects/typespec-vscode.tgz:
|
||||
resolution: {integrity: sha512-0N+mjSTMWK0DFvoeFN95U/kIVyq9sYRB+AWLk8dTe7DHsJFKqm3s801KmFQIEn/gKrbxniC8ig3JcSGveMJzdA==, tarball: file:projects/typespec-vscode.tgz}
|
||||
name: '@rush-temp/typespec-vscode'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@rollup/plugin-commonjs': 23.0.7_rollup@3.4.0
|
||||
'@rollup/plugin-node-resolve': 15.0.1_rollup@3.4.0
|
||||
'@types/mkdirp': 1.0.2
|
||||
'@types/mocha': 10.0.1
|
||||
'@types/node': 18.11.18
|
||||
'@types/vscode': 1.53.0
|
||||
'@vscode/vsce': 2.15.0
|
||||
c8: 7.12.0
|
||||
eslint: 8.33.0
|
||||
mkdirp: 1.0.4
|
||||
mocha: 10.1.0
|
||||
mocha-junit-reporter: 2.2.0_mocha@10.1.0
|
||||
mocha-multi-reporters: 1.5.1_mocha@10.1.0
|
||||
rimraf: 3.0.2
|
||||
rollup: 3.4.0
|
||||
typescript: 4.9.5
|
||||
vscode-languageclient: 8.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
file:projects/compiler.tgz:
|
||||
resolution: {integrity: sha512-i1BeyaHFBrW1eoGxnRhPuNLkeK7eJ12VJQ89i9MRwnaCCkPi15Zloqdqyk+4UoLLeOKjuaJ2JqBR1YlBEGV6ng==, tarball: file:projects/compiler.tgz}
|
||||
resolution: {integrity: sha512-E/67YdnjZc8zQyM9W57MRhwNhI+iZ9Tl2LnmsrY5Dz+AKJ+BqWgEOsOAoLrxOuiXrYhVbtFzjwJf0Jzi8LdySA==, tarball: file:projects/compiler.tgz}
|
||||
name: '@rush-temp/compiler'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
@ -15392,7 +15362,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/eslint-config-typespec.tgz_prettier@2.8.3:
|
||||
resolution: {integrity: sha512-ovAW4RMqoyTK/d6UwLoIiS7KwDgNWLfSt1Q1Pg0NDshJJailWLxtTrgcU5yn/8eL1vm9bRFfbiI0OvOhBsjCYA==, tarball: file:projects/eslint-config-typespec.tgz}
|
||||
resolution: {integrity: sha512-EyCznsDzBazJ943GKlNWfw3DHB7AZmc/RkOHMRs9Lj/F36+MtuNpzcKBqglzqP5ZygaT3QsaQx839rXrx6EGzQ==, tarball: file:projects/eslint-config-typespec.tgz}
|
||||
id: file:projects/eslint-config-typespec.tgz
|
||||
name: '@rush-temp/eslint-config-typespec'
|
||||
version: 0.0.0
|
||||
|
@ -15412,7 +15382,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/eslint-plugin.tgz:
|
||||
resolution: {integrity: sha512-JpaB9Vgn/wscM771iqqV7a0yquhUwfspuFfHnatJaNhNV4mB09q1oyOjz7TxdG3vBUwCrffFgQ937LEu7CHIIA==, tarball: file:projects/eslint-plugin.tgz}
|
||||
resolution: {integrity: sha512-IutMRWMzDh8ekd+G0zQIEVUaYm9eq57oxWdIc9gUWrUODcjS+GTnTkTSqPkuHjVp23r4rPuisOagH4cUzARaPw==, tarball: file:projects/eslint-plugin.tgz}
|
||||
name: '@rush-temp/eslint-plugin'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
@ -15432,7 +15402,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/html-program-viewer.tgz:
|
||||
resolution: {integrity: sha512-Sx0YBNJzEm4H8ob+j0qFBftLTkkAct8jsqZfsyz3bQcWRicqqROdXjc8dq+AHrj5wWTlHrYpsA35owxr6/v0mw==, tarball: file:projects/html-program-viewer.tgz}
|
||||
resolution: {integrity: sha512-ErwYPLrbB7F4e+bqtexPP2lFJluP/ifs0uIrxHV/Z85oQjwum8bdcQRKNEY2YSGFpYy+op1ZLVukL1lEGJRN2g==, tarball: file:projects/html-program-viewer.tgz}
|
||||
name: '@rush-temp/html-program-viewer'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
@ -15458,8 +15428,26 @@ packages:
|
|||
- supports-color
|
||||
dev: false
|
||||
|
||||
file:projects/http.tgz:
|
||||
resolution: {integrity: sha512-+8QdG+qvoJ0GOqYSy/IrdPG79DMrtyDx0IE1486I1TaUfvnj7e7LhXdAkVfEA6NxFZeEkwKGZ7Rxt8JZgTRuUg==, tarball: file:projects/http.tgz}
|
||||
name: '@rush-temp/http'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@types/mocha': 10.0.1
|
||||
'@types/node': 18.11.18
|
||||
c8: 7.12.0
|
||||
eslint: 8.33.0
|
||||
mocha: 10.1.0
|
||||
mocha-junit-reporter: 2.2.0_mocha@10.1.0
|
||||
mocha-multi-reporters: 1.5.1_mocha@10.1.0
|
||||
rimraf: 3.0.2
|
||||
typescript: 4.9.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
file:projects/internal-build-utils.tgz:
|
||||
resolution: {integrity: sha512-AuoNyH0tlfYGipnYP9ZAIWG7fA8A7NwzrSryKCZlQUUbU1ewH3b3cntF1e1kA5SNoU8U9FC49S+FaYTvO7rHpg==, tarball: file:projects/internal-build-utils.tgz}
|
||||
resolution: {integrity: sha512-JN9avh7DaGqtRw3kxRvvpDFdDrKokHIYNe52qpSZU8CBE1n6TQTQgumtjaTC6kxdaG4OpxnMaWAbbnZeu3QyQw==, tarball: file:projects/internal-build-utils.tgz}
|
||||
name: '@rush-temp/internal-build-utils'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
@ -15483,7 +15471,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/library-linter.tgz:
|
||||
resolution: {integrity: sha512-Fka0bz2WHnISX0u4IJzFTvryU1TACZ1Rf0PPossAYZAhpgnTPQc0GTtyxr4MB5iW2TTaWR/OcR8cAFxYn0OypQ==, tarball: file:projects/library-linter.tgz}
|
||||
resolution: {integrity: sha512-YxB1A4KBqtBOahnkriSZuPENCoaqNEI5e5WkLVa5Rzdd1inZoKi3EXXyX+nu1WmbgI3MgS7hoaMVfaIpYWtLxQ==, tarball: file:projects/library-linter.tgz}
|
||||
name: '@rush-temp/library-linter'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
@ -15501,7 +15489,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/lint.tgz:
|
||||
resolution: {integrity: sha512-A5XqSvfcIlQ0zZiEdsFUi/BQYozV9ZlgGH6PTP9r1yjeHF60eRfCn7eQ7qmByAUP4HpuedROQFUc27NeVGhvNQ==, tarball: file:projects/lint.tgz}
|
||||
resolution: {integrity: sha512-FGk655BBWZ30EdulGe5jklyl8JH0DT0vKqckZj8cLqUUtqYVuHIKpgUimH27vuk5wekRR3d1clMkK9Gqjr96uw==, tarball: file:projects/lint.tgz}
|
||||
name: '@rush-temp/lint'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
@ -15519,13 +15507,13 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/migrate.tgz:
|
||||
resolution: {integrity: sha512-1Ma3qCMMfG1Ia4PFP2yap3q38qSa5tRfttPbcz4E6NEkCIzT3v10mAttASpeKvTtlcgc4WOA7yhQNceEsQlSVw==, tarball: file:projects/migrate.tgz}
|
||||
resolution: {integrity: sha512-6q4jXfgG3kBpX8Hg/tj9rsgSEO8Qfl8mJpe7XKrRx9/21MAdhSkocOnBA/IS6CBF5kQMVyumJxAnmH78yeLCsA==, tarball: file:projects/migrate.tgz}
|
||||
name: '@rush-temp/migrate'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@typespec/compiler-v0.37': /@typespec/compiler/0.37.0
|
||||
'@types/mocha': 10.0.1
|
||||
'@types/node': 18.11.18
|
||||
'@typespec/compiler-v0.37': /@cadl-lang/compiler/0.37.0
|
||||
c8: 7.12.0
|
||||
eslint: 8.33.0
|
||||
globby: 13.1.3
|
||||
|
@ -15539,7 +15527,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/openapi.tgz:
|
||||
resolution: {integrity: sha512-CK5gME9cSVV9ZYCnXCaoi6gj0zZ7aSSP5vAGqveADERx513T+EL6Q06Lcp+4xwi45A7KSOOGYdeX9S/gnUejAA==, tarball: file:projects/openapi.tgz}
|
||||
resolution: {integrity: sha512-tqUvUFEZb3kr+MR/bSdTrTEj3HGtgY1jMHiUE8JCQcr09FzxOOHRVvplEu1reHm5y+JISLuXqzvU+1NRfOEIvQ==, tarball: file:projects/openapi.tgz}
|
||||
name: '@rush-temp/openapi'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
@ -15557,7 +15545,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/openapi3.tgz:
|
||||
resolution: {integrity: sha512-GQHFstH+1aQuPdR8JbXabGkjhZpShwtReIOu45pILsbCeXLB44YF0SJ+6uCc8BAagAKzzGnf/PZx0aXqYxYwEA==, tarball: file:projects/openapi3.tgz}
|
||||
resolution: {integrity: sha512-+PHWnKVDGJixPRMlC+6itwq88+fx5YAS4LXV0kZt8Ioa6zXxR51u2A8X8r/Mu/b6+CG6M551odTjvxeVKJ8MoQ==, tarball: file:projects/openapi3.tgz}
|
||||
name: '@rush-temp/openapi3'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
@ -15577,7 +15565,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/playground.tgz_rollup@3.4.0:
|
||||
resolution: {integrity: sha512-5JQ/htgUdR2QkudeSnFYUuncGJAAtc3rH7PH0rKvd80qYrSioVaiJt4B43NDIdgm/kJzbyIVNsZufnCFy6v0vw==, tarball: file:projects/playground.tgz}
|
||||
resolution: {integrity: sha512-c/ILHfQHEWFiRnAzJoFaJmNVbfHxQ2wc/T1a64rMX3VxMHlU+8mxlqAIacoqhRABlUfJWw4itIvU70x3tA/NWQ==, tarball: file:projects/playground.tgz}
|
||||
id: file:projects/playground.tgz
|
||||
name: '@rush-temp/playground'
|
||||
version: 0.0.0
|
||||
|
@ -15633,7 +15621,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/prettier-plugin-typespec.tgz:
|
||||
resolution: {integrity: sha512-GQEQVjZpwz9D0zGQReL53HT5BBX0Q1hPEDggYYbefl1xvnPhnTm6PWtVJxxzsOVnR51X8GXqZGpsSTHnAyJgzQ==, tarball: file:projects/prettier-plugin-typespec.tgz}
|
||||
resolution: {integrity: sha512-8LdJ1gW/+DKqFWba1QEPiADV9i2YYno4NxWQUwt4lAhOiZG9g0m8urceuGcA4uMZ1zujSq3W9XUhjcKi3jHdhA==, tarball: file:projects/prettier-plugin-typespec.tgz}
|
||||
name: '@rush-temp/prettier-plugin-typespec'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
@ -15651,7 +15639,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/ref-doc.tgz:
|
||||
resolution: {integrity: sha512-wS+w8Y2yYQ+VuQMVvzQ9NNH5WZnje28vj0yRGn+TQ0ROnhHEWBqVFDbJ7kiBS6l7Z4VELj6r5lLPpV49xpMcdA==, tarball: file:projects/ref-doc.tgz}
|
||||
resolution: {integrity: sha512-F05bWju+vc/BFJ4CslZBKji0W2zzeUYXYy9m301kHclKjj3a5yxZzgxlvaGLUo1HIrsVterjimr7qfoG1qNcyw==, tarball: file:projects/ref-doc.tgz}
|
||||
name: '@rush-temp/ref-doc'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
@ -15673,7 +15661,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/rest.tgz:
|
||||
resolution: {integrity: sha512-qmAjQIZG/a2eXCuOLa9gv2rsXESSO/N/rhuyljhvaer1Tmy6JgNf55ipsNwiijxPCM7qL4ofb0a73DnuZNSjBA==, tarball: file:projects/rest.tgz}
|
||||
resolution: {integrity: sha512-ptBSo46OFKkgaUZZb0kjmBBuZg/egYcGFIRaYqiS7oaoigs06co1hjT+ji4FE+7NfZAD1jNA38Kd6chFH8SVkw==, tarball: file:projects/rest.tgz}
|
||||
name: '@rush-temp/rest'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
@ -15691,7 +15679,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/samples.tgz:
|
||||
resolution: {integrity: sha512-J8rfWGyUAC6FxDqSpObgDxNOvO0nNJZqbBGKaGuM/4TBIyP5GdSA5uZcC78o4pJxmAfHoUB+JpVDpKeySH+Gtw==, tarball: file:projects/samples.tgz}
|
||||
resolution: {integrity: sha512-lnDPU+AYTd0YwkPNpnzismxBohIgIhxeZPFzLYsFQPm73JIt6nqdECwuu4B8G3kG/KhA0ds9qqy+suF6WI1uTw==, tarball: file:projects/samples.tgz}
|
||||
name: '@rush-temp/samples'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
@ -15703,7 +15691,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/spec.tgz:
|
||||
resolution: {integrity: sha512-v3Z58mWy52wY1DKsE+T0T1K2BTCvjYrYHr4tsih7Zk8QZHZqrhSqfK7tPNTQJ5MLiXdYDusxJZb3inUKmdHClQ==, tarball: file:projects/spec.tgz}
|
||||
resolution: {integrity: sha512-pj6X+JJQIRzYDmPmyx9tcoRvPndJn1RrI/AJApESrfXWUNF7kOnAunmGd7VvESJfpOhFvLl50fAdg9r5cuhNVw==, tarball: file:projects/spec.tgz}
|
||||
name: '@rush-temp/spec'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
@ -15718,7 +15706,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/tmlanguage-generator.tgz:
|
||||
resolution: {integrity: sha512-DejsT0k6kmu6GmONzDQw96JqgEIYqWYjeELVamATe1yDkshqoPDttcj2jnAYWLT1yl+COtWynzb6pTNdUJx6tw==, tarball: file:projects/tmlanguage-generator.tgz}
|
||||
resolution: {integrity: sha512-vr/oeq6/6mo79rEz2Et39cROBjxYr7rwX7NvJkt/cLPvq3CzK22kgixQkyPhScyCZVPW4WRmv2iurKbH8s7G+Q==, tarball: file:projects/tmlanguage-generator.tgz}
|
||||
name: '@rush-temp/tmlanguage-generator'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
@ -15733,8 +15721,40 @@ packages:
|
|||
- supports-color
|
||||
dev: false
|
||||
|
||||
file:projects/typespec-vs.tgz:
|
||||
resolution: {integrity: sha512-S8FACAX4yQTj2TnM1wafi55Z9SFJnTvW4MaDOlIkO2x48DpnbHZaBzSNIn43i4iLLvYcpAnazuFNKk2dwtEiIg==, tarball: file:projects/typespec-vs.tgz}
|
||||
name: '@rush-temp/typespec-vs'
|
||||
version: 0.0.0
|
||||
dev: false
|
||||
|
||||
file:projects/typespec-vscode.tgz:
|
||||
resolution: {integrity: sha512-cKCr5r+q3U04L4+WvAh3YLMHGQW0jP26/eD4YoQzW0qF5TfRg1qTTVAnCONSggiHGR9GGhGVl2kWefA6OHpt3w==, tarball: file:projects/typespec-vscode.tgz}
|
||||
name: '@rush-temp/typespec-vscode'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@rollup/plugin-commonjs': 23.0.7_rollup@3.4.0
|
||||
'@rollup/plugin-node-resolve': 15.0.1_rollup@3.4.0
|
||||
'@types/mkdirp': 1.0.2
|
||||
'@types/mocha': 10.0.1
|
||||
'@types/node': 18.11.18
|
||||
'@types/vscode': 1.53.0
|
||||
'@vscode/vsce': 2.15.0
|
||||
c8: 7.12.0
|
||||
eslint: 8.33.0
|
||||
mkdirp: 1.0.4
|
||||
mocha: 10.1.0
|
||||
mocha-junit-reporter: 2.2.0_mocha@10.1.0
|
||||
mocha-multi-reporters: 1.5.1_mocha@10.1.0
|
||||
rimraf: 3.0.2
|
||||
rollup: 3.4.0
|
||||
typescript: 4.9.5
|
||||
vscode-languageclient: 8.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
file:projects/versioning.tgz:
|
||||
resolution: {integrity: sha512-W9N1Bu61Uv0uucYLOTIUtyWyc90ZMw4IIHpImbxutNRKSwDiB/RezfurWyV00baH309ua3e+Yc2MW0VJZfM9jw==, tarball: file:projects/versioning.tgz}
|
||||
resolution: {integrity: sha512-kZZ3sFk/i3U9Sl0TxlLaxRHEMSmvAVy3EKpFIwhH+7wEQN+cpwZvuBpuCWj263JQK7yIE1js973eKXajZIGPcg==, tarball: file:projects/versioning.tgz}
|
||||
name: '@rush-temp/versioning'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
|
@ -15752,7 +15772,7 @@ packages:
|
|||
dev: false
|
||||
|
||||
file:projects/website.tgz_@types+react@18.0.27:
|
||||
resolution: {integrity: sha512-2QdPBteBUQopsDuQXD+Jd6S60XMMyx4ruWIj/Ys3jcaxAEDI9JUKfWXpz9aR8ySQ5ckj48T63QjsNWo/8fB9yw==, tarball: file:projects/website.tgz}
|
||||
resolution: {integrity: sha512-DP2BOM3gpf89GPfCB2zPKEjHbsnbIovcSyTyh86cAk7hQviHDQvStyVLE4PmTZDM/FNF60uwaKGt4GM48BrYNQ==, tarball: file:projects/website.tgz}
|
||||
id: file:projects/website.tgz
|
||||
name: '@rush-temp/website'
|
||||
version: 0.0.0
|
||||
|
|
|
@ -23,16 +23,16 @@ However, if your emitter does want to split types as OpenAPI does, then it will
|
|||
|
||||
These are the main API involved in handling these features. See the linked API reference documentation for more details.
|
||||
|
||||
- [`getRequestVisibility(HttpVerb): Visibility`](../standard-library/rest/reference/js-api/modules/http.md#getrequestvisibility) - Use this to determine the visibility implied for data in the request parameters or body. Also note that [`Visibility.Read`](../standard-library/rest/reference/js-api/enums/http.Visibility#item) is always applied for response data and therefore there is no corresponding API for the response.
|
||||
- [`getRequestVisibility(HttpVerb): Visibility`](../standard-library/http/reference/js-api/index.md#getrequestvisibility) - Use this to determine the visibility implied for data in the request parameters or body. Also note that [`Visibility.Read`](../standard-library/rest/reference/js-api/enums/http.Visibility#item) is always applied for response data and therefore there is no corresponding API for the response.
|
||||
|
||||
- [`MetadataInfo`](../standard-library/rest/reference/js-api/interfaces/http.MetadataInfo.md) - Create this once for each program using [`createMetadataInfo(Program, MetadataInfoOptions)`](../standard-library/rest/reference/js-api/modules/http.md#createmetadatainfo) then use it to reason about metadata and visibility implications with the API below.
|
||||
- [`MetadataInfo`](../standard-library/http/reference/js-api/interfaces/MetadataInfo.md) - Create this once for each program using [`createMetadataInfo(Program, MetadataInfoOptions)`](../standard-library/http/reference/js-api/index.md#createmetadatainfo) then use it to reason about metadata and visibility implications with the API below.
|
||||
|
||||
- [`MetadataInfo.getEffectivePayloadType(Type, Visibility): Type`](../standard-library/rest/reference/js-api/interfaces/http.MetadataInfo.md#geteffectivepayloadtype) - Use this recursively on every type that is referenced. When given an anonymous model sourced entirely from a single named model after metadata is moved elsewhere or invisible properties are removed, it will recover the named model. This handles the commonly discussed case of seeing that `op something(...Thing)` receives a `Thing` in its request body, but also many other cases.
|
||||
- [`MetadataInfo.getEffectivePayloadType(Type, Visibility): Type`](../standard-library/http/reference/js-api/interfaces/MetadataInfo.md#geteffectivepayloadtype) - Use this recursively on every type that is referenced. When given an anonymous model sourced entirely from a single named model after metadata is moved elsewhere or invisible properties are removed, it will recover the named model. This handles the commonly discussed case of seeing that `op something(...Thing)` receives a `Thing` in its request body, but also many other cases.
|
||||
|
||||
- [`MetadataInfo.isTransformed(Model, Visibility)`](../standard-library/rest/reference/js-api/interfaces/http.MetadataInfo.md#istransformed) - Use this to check if a type undergoes any changes in shape due to visibility or metadata. If not, this can allow for simplifications in emit.
|
||||
- [`MetadataInfo.isTransformed(Model, Visibility)`](../standard-library/http/reference/js-api/interfaces/MetadataInfo.md#istransformed) - Use this to check if a type undergoes any changes in shape due to visibility or metadata. If not, this can allow for simplifications in emit.
|
||||
|
||||
- [`MetadataInfo.isPayloadProperty(ModelProperty, Visibility): boolean`](../standard-library/rest/reference/js-api/interfaces/http.MetadataInfo.md#ispayloadproperty) - Use this to check if a property is transmitted as an object property in the payload and is not invisible or metadata sent elsewhere.
|
||||
- [`MetadataInfo.isPayloadProperty(ModelProperty, Visibility): boolean`](../standard-library/http/reference/js-api/interfaces/MetadataInfo.md#ispayloadproperty) - Use this to check if a property is transmitted as an object property in the payload and is not invisible or metadata sent elsewhere.
|
||||
|
||||
- [`MetadataInfo.isOptional(ModelProperty, Visibility): boolean`](../standard-library/rest/reference/js-api/interfaces/http.MetadataInfo.md#isoptional) - Use this to determine if a property is optional for the given visibility. This will differ from `ModelProperty.isOptional` when the Visibility is Update in which case the property is always considered optional.
|
||||
- [`MetadataInfo.isOptional(ModelProperty, Visibility): boolean`](../standard-library/http/reference/js-api/interfaces/MetadataInfo.md#isoptional) - Use this to determine if a property is optional for the given visibility. This will differ from `ModelProperty.isOptional` when the Visibility is Update in which case the property is always considered optional.
|
||||
|
||||
- [`Visibility.Item`](../standard-library/rest/reference/js-api/enums/http.Visibility#item) - Add this flag when recursing into an array. This moves all metadata into the payload, which can be useful in scenarios like batching API calls.
|
||||
|
|
|
@ -70,9 +70,7 @@ namespace Pets {
|
|||
|
||||
### Automatic route generation
|
||||
|
||||
Instead of manually specifying routes using the `@route` decorator, you automatically generate
|
||||
routes from operation parameters by applying the `@autoRoute` decorator to an operation, namespace,
|
||||
or interface containing operations.
|
||||
Instead of manually specifying routes using the `@route` decorator, you automatically generate routes from operation parameters by applying the `@autoRoute` decorator to an operation or interface containing operations.
|
||||
|
||||
For this to work, an operation's path parameters (those marked with `@path`) must also be marked with
|
||||
the `@segment` decorator to define the preceding path segment.
|
||||
|
|
|
@ -0,0 +1,275 @@
|
|||
---
|
||||
title: "Data types"
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 3
|
||||
---
|
||||
|
||||
# Data types
|
||||
|
||||
## TypeSpec.Http
|
||||
|
||||
### `Response` {#TypeSpec.Http.Response}
|
||||
|
||||
```typespec
|
||||
model Response<Status>
|
||||
```
|
||||
|
||||
#### Template Parameters
|
||||
|
||||
| Name | Description |
|
||||
| ------ | ----------- |
|
||||
| Status | |
|
||||
|
||||
### `Body` {#TypeSpec.Http.Body}
|
||||
|
||||
Defines a model with a single property of the given type, marked with `@body`.
|
||||
|
||||
This can be useful in situations where you cannot use a bare T as the body
|
||||
and it is awkward to add a property.
|
||||
|
||||
```typespec
|
||||
model Body<T>
|
||||
```
|
||||
|
||||
#### Template Parameters
|
||||
|
||||
| Name | Description |
|
||||
| ---- | ----------- |
|
||||
| T | |
|
||||
|
||||
### `LocationHeader` {#TypeSpec.Http.LocationHeader}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.LocationHeader
|
||||
```
|
||||
|
||||
### `HeaderOptions` {#TypeSpec.Http.HeaderOptions}
|
||||
|
||||
Header options.
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.HeaderOptions
|
||||
```
|
||||
|
||||
### `OkResponse` {#TypeSpec.Http.OkResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.OkResponse
|
||||
```
|
||||
|
||||
### `CreatedResponse` {#TypeSpec.Http.CreatedResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.CreatedResponse
|
||||
```
|
||||
|
||||
### `AcceptedResponse` {#TypeSpec.Http.AcceptedResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.AcceptedResponse
|
||||
```
|
||||
|
||||
### `NoContentResponse` {#TypeSpec.Http.NoContentResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.NoContentResponse
|
||||
```
|
||||
|
||||
### `MovedResponse` {#TypeSpec.Http.MovedResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.MovedResponse
|
||||
```
|
||||
|
||||
### `NotModifiedResponse` {#TypeSpec.Http.NotModifiedResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.NotModifiedResponse
|
||||
```
|
||||
|
||||
### `BadRequestResponse` {#TypeSpec.Http.BadRequestResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.BadRequestResponse
|
||||
```
|
||||
|
||||
### `UnauthorizedResponse` {#TypeSpec.Http.UnauthorizedResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.UnauthorizedResponse
|
||||
```
|
||||
|
||||
### `ForbiddenResponse` {#TypeSpec.Http.ForbiddenResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.ForbiddenResponse
|
||||
```
|
||||
|
||||
### `NotFoundResponse` {#TypeSpec.Http.NotFoundResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.NotFoundResponse
|
||||
```
|
||||
|
||||
### `ConflictResponse` {#TypeSpec.Http.ConflictResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.ConflictResponse
|
||||
```
|
||||
|
||||
### `PlainData` {#TypeSpec.Http.PlainData}
|
||||
|
||||
Produces a new model with the same properties as T, but with `@query`,
|
||||
`@header`, `@body`, and `@path` decorators removed from all properties.
|
||||
|
||||
```typespec
|
||||
model PlainData<T>
|
||||
```
|
||||
|
||||
#### Template Parameters
|
||||
|
||||
| Name | Description |
|
||||
| ---- | ----------- |
|
||||
| T | |
|
||||
|
||||
### `QueryOptions` {#TypeSpec.Http.QueryOptions}
|
||||
|
||||
Query parameter options.
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.QueryOptions
|
||||
```
|
||||
|
||||
### `BasicAuth` {#TypeSpec.Http.BasicAuth}
|
||||
|
||||
Basic authentication is a simple authentication scheme built into the HTTP protocol.
|
||||
The client sends HTTP requests with the Authorization header that contains the word Basic word followed by a space and a base64-encoded string username:password.
|
||||
For example, to authorize as demo / `p@55w0rd` the client would send
|
||||
|
||||
```
|
||||
Authorization: Basic ZGVtbzpwQDU1dzByZA==
|
||||
```
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.BasicAuth
|
||||
```
|
||||
|
||||
### `BearerAuth` {#TypeSpec.Http.BearerAuth}
|
||||
|
||||
Bearer authentication (also called token authentication) is an HTTP authentication scheme that involves security tokens called bearer tokens.
|
||||
The name “Bearer authentication” can be understood as “give access to the bearer of this token.” The bearer token is a cryptic string, usually generated by the server in response to a login request.
|
||||
The client must send this token in the Authorization header when making requests to protected resources:
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.BearerAuth
|
||||
```
|
||||
|
||||
### `ApiKeyAuth` {#TypeSpec.Http.ApiKeyAuth}
|
||||
|
||||
An API key is a token that a client provides when making API calls. The key can be sent in the query string:
|
||||
|
||||
```
|
||||
GET /something?api_key=abcdef12345
|
||||
```
|
||||
|
||||
or as a request header
|
||||
|
||||
```
|
||||
GET /something HTTP/1.1
|
||||
X-API-Key: abcdef12345
|
||||
```
|
||||
|
||||
or as a cookie
|
||||
|
||||
```
|
||||
GET /something HTTP/1.1
|
||||
Cookie: X-API-KEY=abcdef12345
|
||||
```
|
||||
|
||||
```typespec
|
||||
model ApiKeyAuth<TLocation, TName>
|
||||
```
|
||||
|
||||
#### Template Parameters
|
||||
|
||||
| Name | Description |
|
||||
| --------- | ----------- |
|
||||
| TLocation | |
|
||||
| TName | |
|
||||
|
||||
### `OAuth2Auth` {#TypeSpec.Http.OAuth2Auth}
|
||||
|
||||
OAuth 2.0 is an authorization protocol that gives an API client limited access to user data on a web server.
|
||||
OAuth relies on authentication scenarios called flows, which allow the resource owner (user) to share the protected content from the resource server without sharing their credentials.
|
||||
For that purpose, an OAuth 2.0 server issues access tokens that the client applications can use to access protected resources on behalf of the resource owner.
|
||||
For more information about OAuth 2.0, see oauth.net and RFC 6749.
|
||||
|
||||
```typespec
|
||||
model OAuth2Auth<TFlows>
|
||||
```
|
||||
|
||||
#### Template Parameters
|
||||
|
||||
| Name | Description |
|
||||
| ------ | ----------- |
|
||||
| TFlows | |
|
||||
|
||||
### `AuthorizationCodeFlow` {#TypeSpec.Http.AuthorizationCodeFlow}
|
||||
|
||||
Authorization Code flow
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.AuthorizationCodeFlow
|
||||
```
|
||||
|
||||
### `ImplicitFlow` {#TypeSpec.Http.ImplicitFlow}
|
||||
|
||||
Implicit flow
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.ImplicitFlow
|
||||
```
|
||||
|
||||
### `PasswordFlow` {#TypeSpec.Http.PasswordFlow}
|
||||
|
||||
Resource Owner Password flow
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.PasswordFlow
|
||||
```
|
||||
|
||||
### `ClientCredentialsFlow` {#TypeSpec.Http.ClientCredentialsFlow}
|
||||
|
||||
Client credentials flow
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.ClientCredentialsFlow
|
||||
```
|
||||
|
||||
### `AuthType` {#TypeSpec.Http.AuthType}
|
||||
|
||||
Authentication type
|
||||
|
||||
```typespec
|
||||
enum TypeSpec.Http.AuthType
|
||||
```
|
||||
|
||||
### `ApiKeyLocation` {#TypeSpec.Http.ApiKeyLocation}
|
||||
|
||||
Describes the location of the API key
|
||||
|
||||
```typespec
|
||||
enum TypeSpec.Http.ApiKeyLocation
|
||||
```
|
||||
|
||||
### `OAuth2FlowType` {#TypeSpec.Http.OAuth2FlowType}
|
||||
|
||||
Describes the OAuth2 flow type
|
||||
|
||||
```typespec
|
||||
enum TypeSpec.Http.OAuth2FlowType
|
||||
```
|
|
@ -0,0 +1,305 @@
|
|||
---
|
||||
title: "Decorators"
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 3
|
||||
---
|
||||
|
||||
# Decorators
|
||||
|
||||
## TypeSpec.Http
|
||||
|
||||
### `@statusCode` {#@TypeSpec.Http.statusCode}
|
||||
|
||||
Specify the status code for this response. Property type must be a status code integer or a union of status code integer.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.statusCode(target: TypeSpec.Reflection.ModelProperty)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`ModelProperty`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
|
||||
#### Examples
|
||||
|
||||
```typespec
|
||||
op read(): {@statusCode: 200, @body pet: Pet}
|
||||
op create(): {@statusCode: 201 | 202}
|
||||
```
|
||||
|
||||
### `@body` {#@TypeSpec.Http.body}
|
||||
|
||||
Explicitly specify that this property is to be set as the body
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.body(target: TypeSpec.Reflection.ModelProperty)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`ModelProperty`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
|
||||
#### Examples
|
||||
|
||||
```typespec
|
||||
op upload(@body image: bytes): void;
|
||||
op download(): {@body image: bytes};
|
||||
```
|
||||
|
||||
### `@header` {#@TypeSpec.Http.header}
|
||||
|
||||
Specify this property is to be sent or received as an http header.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.header(target: TypeSpec.Reflection.ModelProperty, headerNameOrOptions?: TypeSpec.string | TypeSpec.Http.HeaderOptions)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`ModelProperty`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------------------- | ------------------------------------------------------ | ------------------------------------------------------------------ |
|
||||
| headerNameOrOptions | `union TypeSpec.string \| TypeSpec.Http.HeaderOptions` | Optional name of the header when sent over http or header options. |
|
||||
|
||||
### `@query` {#@TypeSpec.Http.query}
|
||||
|
||||
Specify this property is to be sent as a query parameter.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.query(target: TypeSpec.Reflection.ModelProperty, queryNameOrOptions?: TypeSpec.string | TypeSpec.Http.QueryOptions)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`ModelProperty`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| queryNameOrOptions | `union TypeSpec.string \| TypeSpec.Http.QueryOptions` | Optional name of the query when included in the url or query parameter options. |
|
||||
|
||||
### `@path` {#@TypeSpec.Http.path}
|
||||
|
||||
Explicitly specify that this property is to be interpolated as a path parameter.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.path(target: TypeSpec.Reflection.ModelProperty, paramName?: TypeSpec.string)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`ModelProperty`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| --------- | ------------------------ | --------------------------------------------------- |
|
||||
| paramName | `scalar TypeSpec.string` | Optional name of the parameter in the url template. |
|
||||
|
||||
### `@get` {#@TypeSpec.Http.get}
|
||||
|
||||
Specify the http verb for the target operation to be `GET`.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.get(target: TypeSpec.Reflection.Operation)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`Operation`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
|
||||
#### Examples
|
||||
|
||||
```typespec
|
||||
@get op read(): string
|
||||
```
|
||||
|
||||
### `@put` {#@TypeSpec.Http.put}
|
||||
|
||||
Specify the http verb for the target operation to be `PUT`.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.put(target: TypeSpec.Reflection.Operation)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`Operation`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
|
||||
#### Examples
|
||||
|
||||
```typespec
|
||||
@put op set(pet: Pet): void
|
||||
```
|
||||
|
||||
### `@post` {#@TypeSpec.Http.post}
|
||||
|
||||
Specify the http verb for the target operation to be `POST`.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.post(target: TypeSpec.Reflection.Operation)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`Operation`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
|
||||
#### Examples
|
||||
|
||||
```typespec
|
||||
@post op create(pet: Pet): void
|
||||
```
|
||||
|
||||
### `@patch` {#@TypeSpec.Http.patch}
|
||||
|
||||
Specify the http verb for the target operation to be `PATCH`.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.patch(target: TypeSpec.Reflection.Operation)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`Operation`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
|
||||
#### Examples
|
||||
|
||||
```typespec
|
||||
@patch op update(pet: Pet): void
|
||||
```
|
||||
|
||||
### `@delete` {#@TypeSpec.Http.delete}
|
||||
|
||||
Specify the http verb for the target operation to be `DELETE`.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.delete(target: TypeSpec.Reflection.Operation)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`Operation`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
|
||||
#### Examples
|
||||
|
||||
```typespec
|
||||
@delete op set(petId: string): void
|
||||
```
|
||||
|
||||
### `@head` {#@TypeSpec.Http.head}
|
||||
|
||||
Specify the http verb for the target operation to be `HEAD`.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.head(target: TypeSpec.Reflection.Operation)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`Operation`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
|
||||
#### Examples
|
||||
|
||||
```typespec
|
||||
@head op ping(petId: string): void
|
||||
```
|
||||
|
||||
### `@server` {#@TypeSpec.Http.server}
|
||||
|
||||
Specify the endpoint for this service.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.server(target: TypeSpec.Reflection.Namespace, url: TypeSpec.string, description: TypeSpec.string, parameters?: TypeSpec.object)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`Namespace`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ----------- | ------------------------ | ------------------------------------------------------- |
|
||||
| url | `scalar TypeSpec.string` | Description of the endpoint |
|
||||
| description | `scalar TypeSpec.string` | |
|
||||
| parameters | `model TypeSpec.object` | Optional set of parameters used to interpolate the url. |
|
||||
|
||||
### `@useAuth` {#@TypeSpec.Http.useAuth}
|
||||
|
||||
Specify this service authentication. See the [documentation in the Http library][https://microsoft.github.io/typespec/standard-library/rest/authentication] for full details.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.useAuth(target: TypeSpec.Reflection.Namespace, auth: TypeSpec.object | TypeSpec.Reflection.Union | TypeSpec.object[])
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`Namespace`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| auth | `union TypeSpec.object \| TypeSpec.Reflection.Union \| TypeSpec.object[]` | Authentication configuration. Can be a single security scheme, a union(either option is valid authentication) or a tuple(Must use all authentication together) |
|
||||
|
||||
### `@includeInapplicableMetadataInPayload` {#@TypeSpec.Http.includeInapplicableMetadataInPayload}
|
||||
|
||||
Specify if inapplicable metadata should be included in the payload for the given entity.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.includeInapplicableMetadataInPayload(target: unknown, value: TypeSpec.boolean)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`(intrinsic) unknown`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ----- | ------------------------- | ----------- |
|
||||
| value | `scalar TypeSpec.boolean` | |
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
title: Index
|
||||
sidebar_position: 0
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 3
|
||||
---
|
||||
|
||||
## TypeSpec.Http
|
||||
|
||||
### Decorators
|
||||
|
||||
- [`@statusCode`](./decorators.md#@TypeSpec.Http.statusCode)
|
||||
- [`@body`](./decorators.md#@TypeSpec.Http.body)
|
||||
- [`@header`](./decorators.md#@TypeSpec.Http.header)
|
||||
- [`@query`](./decorators.md#@TypeSpec.Http.query)
|
||||
- [`@path`](./decorators.md#@TypeSpec.Http.path)
|
||||
- [`@get`](./decorators.md#@TypeSpec.Http.get)
|
||||
- [`@put`](./decorators.md#@TypeSpec.Http.put)
|
||||
- [`@post`](./decorators.md#@TypeSpec.Http.post)
|
||||
- [`@patch`](./decorators.md#@TypeSpec.Http.patch)
|
||||
- [`@delete`](./decorators.md#@TypeSpec.Http.delete)
|
||||
- [`@head`](./decorators.md#@TypeSpec.Http.head)
|
||||
- [`@server`](./decorators.md#@TypeSpec.Http.server)
|
||||
- [`@useAuth`](./decorators.md#@TypeSpec.Http.useAuth)
|
||||
- [`@includeInapplicableMetadataInPayload`](./decorators.md#@TypeSpec.Http.includeInapplicableMetadataInPayload)
|
||||
|
||||
### Models
|
||||
|
||||
- [`Response`](./data-types.md#TypeSpec.Http.Response)
|
||||
- [`Body`](./data-types.md#TypeSpec.Http.Body)
|
||||
- [`LocationHeader`](./data-types.md#TypeSpec.Http.LocationHeader)
|
||||
- [`HeaderOptions`](./data-types.md#TypeSpec.Http.HeaderOptions)
|
||||
- [`OkResponse`](./data-types.md#TypeSpec.Http.OkResponse)
|
||||
- [`CreatedResponse`](./data-types.md#TypeSpec.Http.CreatedResponse)
|
||||
- [`AcceptedResponse`](./data-types.md#TypeSpec.Http.AcceptedResponse)
|
||||
- [`NoContentResponse`](./data-types.md#TypeSpec.Http.NoContentResponse)
|
||||
- [`MovedResponse`](./data-types.md#TypeSpec.Http.MovedResponse)
|
||||
- [`NotModifiedResponse`](./data-types.md#TypeSpec.Http.NotModifiedResponse)
|
||||
- [`BadRequestResponse`](./data-types.md#TypeSpec.Http.BadRequestResponse)
|
||||
- [`UnauthorizedResponse`](./data-types.md#TypeSpec.Http.UnauthorizedResponse)
|
||||
- [`ForbiddenResponse`](./data-types.md#TypeSpec.Http.ForbiddenResponse)
|
||||
- [`NotFoundResponse`](./data-types.md#TypeSpec.Http.NotFoundResponse)
|
||||
- [`ConflictResponse`](./data-types.md#TypeSpec.Http.ConflictResponse)
|
||||
- [`PlainData`](./data-types.md#TypeSpec.Http.PlainData)
|
||||
- [`QueryOptions`](./data-types.md#TypeSpec.Http.QueryOptions)
|
||||
- [`BasicAuth`](./data-types.md#TypeSpec.Http.BasicAuth)
|
||||
- [`BearerAuth`](./data-types.md#TypeSpec.Http.BearerAuth)
|
||||
- [`ApiKeyAuth`](./data-types.md#TypeSpec.Http.ApiKeyAuth)
|
||||
- [`OAuth2Auth`](./data-types.md#TypeSpec.Http.OAuth2Auth)
|
||||
- [`AuthorizationCodeFlow`](./data-types.md#TypeSpec.Http.AuthorizationCodeFlow)
|
||||
- [`ImplicitFlow`](./data-types.md#TypeSpec.Http.ImplicitFlow)
|
||||
- [`PasswordFlow`](./data-types.md#TypeSpec.Http.PasswordFlow)
|
||||
- [`ClientCredentialsFlow`](./data-types.md#TypeSpec.Http.ClientCredentialsFlow)
|
|
@ -6,274 +6,6 @@ toc_max_heading_level: 3
|
|||
|
||||
# Data types
|
||||
|
||||
## TypeSpec.Http
|
||||
|
||||
### `Response` {#TypeSpec.Http.Response}
|
||||
|
||||
```typespec
|
||||
model Response<Status>
|
||||
```
|
||||
|
||||
#### Template Parameters
|
||||
|
||||
| Name | Description |
|
||||
| ------ | ----------- |
|
||||
| Status | |
|
||||
|
||||
### `Body` {#TypeSpec.Http.Body}
|
||||
|
||||
Defines a model with a single property of the given type, marked with `@body`.
|
||||
|
||||
This can be useful in situations where you cannot use a bare T as the body
|
||||
and it is awkward to add a property.
|
||||
|
||||
```typespec
|
||||
model Body<T>
|
||||
```
|
||||
|
||||
#### Template Parameters
|
||||
|
||||
| Name | Description |
|
||||
| ---- | ----------- |
|
||||
| T | |
|
||||
|
||||
### `LocationHeader` {#TypeSpec.Http.LocationHeader}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.LocationHeader
|
||||
```
|
||||
|
||||
### `HeaderOptions` {#TypeSpec.Http.HeaderOptions}
|
||||
|
||||
Header options.
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.HeaderOptions
|
||||
```
|
||||
|
||||
### `OkResponse` {#TypeSpec.Http.OkResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.OkResponse
|
||||
```
|
||||
|
||||
### `CreatedResponse` {#TypeSpec.Http.CreatedResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.CreatedResponse
|
||||
```
|
||||
|
||||
### `AcceptedResponse` {#TypeSpec.Http.AcceptedResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.AcceptedResponse
|
||||
```
|
||||
|
||||
### `NoContentResponse` {#TypeSpec.Http.NoContentResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.NoContentResponse
|
||||
```
|
||||
|
||||
### `MovedResponse` {#TypeSpec.Http.MovedResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.MovedResponse
|
||||
```
|
||||
|
||||
### `NotModifiedResponse` {#TypeSpec.Http.NotModifiedResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.NotModifiedResponse
|
||||
```
|
||||
|
||||
### `BadRequestResponse` {#TypeSpec.Http.BadRequestResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.BadRequestResponse
|
||||
```
|
||||
|
||||
### `UnauthorizedResponse` {#TypeSpec.Http.UnauthorizedResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.UnauthorizedResponse
|
||||
```
|
||||
|
||||
### `ForbiddenResponse` {#TypeSpec.Http.ForbiddenResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.ForbiddenResponse
|
||||
```
|
||||
|
||||
### `NotFoundResponse` {#TypeSpec.Http.NotFoundResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.NotFoundResponse
|
||||
```
|
||||
|
||||
### `ConflictResponse` {#TypeSpec.Http.ConflictResponse}
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.ConflictResponse
|
||||
```
|
||||
|
||||
### `PlainData` {#TypeSpec.Http.PlainData}
|
||||
|
||||
Produces a new model with the same properties as T, but with `@query`,
|
||||
`@header`, `@body`, and `@path` decorators removed from all properties.
|
||||
|
||||
```typespec
|
||||
model PlainData<T>
|
||||
```
|
||||
|
||||
#### Template Parameters
|
||||
|
||||
| Name | Description |
|
||||
| ---- | ----------- |
|
||||
| T | |
|
||||
|
||||
### `QueryOptions` {#TypeSpec.Http.QueryOptions}
|
||||
|
||||
Query parameter options.
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.QueryOptions
|
||||
```
|
||||
|
||||
### `BasicAuth` {#TypeSpec.Http.BasicAuth}
|
||||
|
||||
Basic authentication is a simple authentication scheme built into the HTTP protocol.
|
||||
The client sends HTTP requests with the Authorization header that contains the word Basic word followed by a space and a base64-encoded string username:password.
|
||||
For example, to authorize as demo / `p@55w0rd` the client would send
|
||||
|
||||
```
|
||||
Authorization: Basic ZGVtbzpwQDU1dzByZA==
|
||||
```
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.BasicAuth
|
||||
```
|
||||
|
||||
### `BearerAuth` {#TypeSpec.Http.BearerAuth}
|
||||
|
||||
Bearer authentication (also called token authentication) is an HTTP authentication scheme that involves security tokens called bearer tokens.
|
||||
The name “Bearer authentication” can be understood as “give access to the bearer of this token.” The bearer token is a cryptic string, usually generated by the server in response to a login request.
|
||||
The client must send this token in the Authorization header when making requests to protected resources:
|
||||
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.BearerAuth
|
||||
```
|
||||
|
||||
### `ApiKeyAuth` {#TypeSpec.Http.ApiKeyAuth}
|
||||
|
||||
An API key is a token that a client provides when making API calls. The key can be sent in the query string:
|
||||
|
||||
```
|
||||
GET /something?api_key=abcdef12345
|
||||
```
|
||||
|
||||
or as a request header
|
||||
|
||||
```
|
||||
GET /something HTTP/1.1
|
||||
X-API-Key: abcdef12345
|
||||
```
|
||||
|
||||
or as a cookie
|
||||
|
||||
```
|
||||
GET /something HTTP/1.1
|
||||
Cookie: X-API-KEY=abcdef12345
|
||||
```
|
||||
|
||||
```typespec
|
||||
model ApiKeyAuth<TLocation, TName>
|
||||
```
|
||||
|
||||
#### Template Parameters
|
||||
|
||||
| Name | Description |
|
||||
| --------- | ----------- |
|
||||
| TLocation | |
|
||||
| TName | |
|
||||
|
||||
### `OAuth2Auth` {#TypeSpec.Http.OAuth2Auth}
|
||||
|
||||
OAuth 2.0 is an authorization protocol that gives an API client limited access to user data on a web server.
|
||||
OAuth relies on authentication scenarios called flows, which allow the resource owner (user) to share the protected content from the resource server without sharing their credentials.
|
||||
For that purpose, an OAuth 2.0 server issues access tokens that the client applications can use to access protected resources on behalf of the resource owner.
|
||||
For more information about OAuth 2.0, see oauth.net and RFC 6749.
|
||||
|
||||
```typespec
|
||||
model OAuth2Auth<TFlows>
|
||||
```
|
||||
|
||||
#### Template Parameters
|
||||
|
||||
| Name | Description |
|
||||
| ------ | ----------- |
|
||||
| TFlows | |
|
||||
|
||||
### `AuthorizationCodeFlow` {#TypeSpec.Http.AuthorizationCodeFlow}
|
||||
|
||||
Authorization Code flow
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.AuthorizationCodeFlow
|
||||
```
|
||||
|
||||
### `ImplicitFlow` {#TypeSpec.Http.ImplicitFlow}
|
||||
|
||||
Implicit flow
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.ImplicitFlow
|
||||
```
|
||||
|
||||
### `PasswordFlow` {#TypeSpec.Http.PasswordFlow}
|
||||
|
||||
Resource Owner Password flow
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.PasswordFlow
|
||||
```
|
||||
|
||||
### `ClientCredentialsFlow` {#TypeSpec.Http.ClientCredentialsFlow}
|
||||
|
||||
Client credentials flow
|
||||
|
||||
```typespec
|
||||
model TypeSpec.Http.ClientCredentialsFlow
|
||||
```
|
||||
|
||||
### `AuthType` {#TypeSpec.Http.AuthType}
|
||||
|
||||
Authentication type
|
||||
|
||||
```typespec
|
||||
enum TypeSpec.Http.AuthType
|
||||
```
|
||||
|
||||
### `ApiKeyLocation` {#TypeSpec.Http.ApiKeyLocation}
|
||||
|
||||
Describes the location of the API key
|
||||
|
||||
```typespec
|
||||
enum TypeSpec.Http.ApiKeyLocation
|
||||
```
|
||||
|
||||
### `OAuth2FlowType` {#TypeSpec.Http.OAuth2FlowType}
|
||||
|
||||
Describes the OAuth2 flow type
|
||||
|
||||
```typespec
|
||||
enum TypeSpec.Http.OAuth2FlowType
|
||||
```
|
||||
|
||||
## TypeSpec.Rest.Resource
|
||||
|
||||
### `ResourceError` {#TypeSpec.Rest.Resource.ResourceError}
|
||||
|
|
|
@ -6,317 +6,19 @@ toc_max_heading_level: 3
|
|||
|
||||
# Decorators
|
||||
|
||||
## TypeSpec.Http
|
||||
|
||||
### `@statusCode` {#@TypeSpec.Http.statusCode}
|
||||
|
||||
Specify the status code for this response. Property type must be a status code integer or a union of status code integer.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.statusCode(target: TypeSpec.Reflection.ModelProperty)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`ModelProperty`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
|
||||
#### Examples
|
||||
|
||||
```typespec
|
||||
op read(): {@statusCode: 200, @body pet: Pet}
|
||||
op create(): {@statusCode: 201 | 202}
|
||||
```
|
||||
|
||||
### `@body` {#@TypeSpec.Http.body}
|
||||
|
||||
Explicitly specify that this property is to be set as the body
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.body(target: TypeSpec.Reflection.ModelProperty)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`ModelProperty`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
|
||||
#### Examples
|
||||
|
||||
```typespec
|
||||
op upload(@body image: bytes): void;
|
||||
op download(): {@body image: bytes};
|
||||
```
|
||||
|
||||
### `@header` {#@TypeSpec.Http.header}
|
||||
|
||||
Specify this property is to be sent or received as an http header.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.header(target: TypeSpec.Reflection.ModelProperty, headerNameOrOptions?: TypeSpec.string | TypeSpec.Http.HeaderOptions)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`ModelProperty`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------------------- | ------------------------------------------------------ | ------------------------------------------------------------------ |
|
||||
| headerNameOrOptions | `union TypeSpec.string \| TypeSpec.Http.HeaderOptions` | Optional name of the header when sent over http or header options. |
|
||||
|
||||
### `@query` {#@TypeSpec.Http.query}
|
||||
|
||||
Specify this property is to be sent as a query parameter.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.query(target: TypeSpec.Reflection.ModelProperty, queryNameOrOptions?: TypeSpec.string | TypeSpec.Http.QueryOptions)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`ModelProperty`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| queryNameOrOptions | `union TypeSpec.string \| TypeSpec.Http.QueryOptions` | Optional name of the query when included in the url or query parameter options. |
|
||||
|
||||
### `@path` {#@TypeSpec.Http.path}
|
||||
|
||||
Explicitly specify that this property is to be interpolated as a path parameter.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.path(target: TypeSpec.Reflection.ModelProperty, paramName?: TypeSpec.string)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`ModelProperty`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| --------- | ------------------------ | --------------------------------------------------- |
|
||||
| paramName | `scalar TypeSpec.string` | Optional name of the parameter in the url template. |
|
||||
|
||||
### `@get` {#@TypeSpec.Http.get}
|
||||
|
||||
Specify the http verb for the target operation to be `GET`.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.get(target: TypeSpec.Reflection.Operation)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`Operation`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
|
||||
#### Examples
|
||||
|
||||
```typespec
|
||||
@get op read(): string
|
||||
```
|
||||
|
||||
### `@put` {#@TypeSpec.Http.put}
|
||||
|
||||
Specify the http verb for the target operation to be `PUT`.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.put(target: TypeSpec.Reflection.Operation)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`Operation`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
|
||||
#### Examples
|
||||
|
||||
```typespec
|
||||
@put op set(pet: Pet): void
|
||||
```
|
||||
|
||||
### `@post` {#@TypeSpec.Http.post}
|
||||
|
||||
Specify the http verb for the target operation to be `POST`.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.post(target: TypeSpec.Reflection.Operation)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`Operation`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
|
||||
#### Examples
|
||||
|
||||
```typespec
|
||||
@post op create(pet: Pet): void
|
||||
```
|
||||
|
||||
### `@patch` {#@TypeSpec.Http.patch}
|
||||
|
||||
Specify the http verb for the target operation to be `PATCH`.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.patch(target: TypeSpec.Reflection.Operation)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`Operation`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
|
||||
#### Examples
|
||||
|
||||
```typespec
|
||||
@patch op update(pet: Pet): void
|
||||
```
|
||||
|
||||
### `@delete` {#@TypeSpec.Http.delete}
|
||||
|
||||
Specify the http verb for the target operation to be `DELETE`.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.delete(target: TypeSpec.Reflection.Operation)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`Operation`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
|
||||
#### Examples
|
||||
|
||||
```typespec
|
||||
@delete op set(petId: string): void
|
||||
```
|
||||
|
||||
### `@head` {#@TypeSpec.Http.head}
|
||||
|
||||
Specify the http verb for the target operation to be `HEAD`.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.head(target: TypeSpec.Reflection.Operation)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`Operation`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
|
||||
#### Examples
|
||||
|
||||
```typespec
|
||||
@head op ping(petId: string): void
|
||||
```
|
||||
|
||||
### `@server` {#@TypeSpec.Http.server}
|
||||
|
||||
Specify the endpoint for this service.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.server(target: TypeSpec.Reflection.Namespace, url: TypeSpec.string, description: TypeSpec.string, parameters?: TypeSpec.object)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`Namespace`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ----------- | ------------------------ | ------------------------------------------------------- |
|
||||
| url | `scalar TypeSpec.string` | Description of the endpoint |
|
||||
| description | `scalar TypeSpec.string` | |
|
||||
| parameters | `model TypeSpec.object` | Optional set of parameters used to interpolate the url. |
|
||||
|
||||
### `@useAuth` {#@TypeSpec.Http.useAuth}
|
||||
|
||||
Specify this service authentication. See the [documentation in the Http library][https://microsoft.github.io/typespec/standard-library/rest/authentication] for full details.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.useAuth(target: TypeSpec.Reflection.Namespace, auth: TypeSpec.object | TypeSpec.Reflection.Union | TypeSpec.object[])
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`Namespace`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| auth | `union TypeSpec.object \| TypeSpec.Reflection.Union \| TypeSpec.object[]` | Authentication configuration. Can be a single security scheme, a union(either option is valid authentication) or a tuple(Must use all authentication together) |
|
||||
|
||||
### `@includeInapplicableMetadataInPayload` {#@TypeSpec.Http.includeInapplicableMetadataInPayload}
|
||||
|
||||
Specify if inapplicable metadata should be included in the payload for the given entity.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Http.includeInapplicableMetadataInPayload(target: unknown, value: TypeSpec.boolean)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`(intrinsic) unknown`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| ----- | ------------------------- | ----------- |
|
||||
| value | `scalar TypeSpec.boolean` | |
|
||||
|
||||
## TypeSpec.Rest
|
||||
|
||||
### `@autoRoute` {#@TypeSpec.Rest.autoRoute}
|
||||
|
||||
This namespace, interface or operation should resolve its route automatically. To be used with resource types where the route segments area defined on the models.
|
||||
This interface or operation should resolve its route automatically. To be used with resource types where the route segments area defined on the models.
|
||||
|
||||
```typespec
|
||||
dec TypeSpec.Rest.autoRoute(target: TypeSpec.Reflection.Namespace | TypeSpec.Reflection.Interface | TypeSpec.Reflection.Operation)
|
||||
dec TypeSpec.Rest.autoRoute(target: TypeSpec.Reflection.Interface | TypeSpec.Reflection.Operation)
|
||||
```
|
||||
|
||||
#### Target
|
||||
|
||||
`union TypeSpec.Reflection.Namespace | TypeSpec.Reflection.Interface | TypeSpec.Reflection.Operation`
|
||||
`union TypeSpec.Reflection.Interface | TypeSpec.Reflection.Operation`
|
||||
|
||||
#### Parameters
|
||||
|
||||
|
|
|
@ -5,53 +5,6 @@ toc_min_heading_level: 2
|
|||
toc_max_heading_level: 3
|
||||
---
|
||||
|
||||
## TypeSpec.Http
|
||||
|
||||
### Decorators
|
||||
|
||||
- [`@statusCode`](./decorators.md#@TypeSpec.Http.statusCode)
|
||||
- [`@body`](./decorators.md#@TypeSpec.Http.body)
|
||||
- [`@header`](./decorators.md#@TypeSpec.Http.header)
|
||||
- [`@query`](./decorators.md#@TypeSpec.Http.query)
|
||||
- [`@path`](./decorators.md#@TypeSpec.Http.path)
|
||||
- [`@get`](./decorators.md#@TypeSpec.Http.get)
|
||||
- [`@put`](./decorators.md#@TypeSpec.Http.put)
|
||||
- [`@post`](./decorators.md#@TypeSpec.Http.post)
|
||||
- [`@patch`](./decorators.md#@TypeSpec.Http.patch)
|
||||
- [`@delete`](./decorators.md#@TypeSpec.Http.delete)
|
||||
- [`@head`](./decorators.md#@TypeSpec.Http.head)
|
||||
- [`@server`](./decorators.md#@TypeSpec.Http.server)
|
||||
- [`@useAuth`](./decorators.md#@TypeSpec.Http.useAuth)
|
||||
- [`@includeInapplicableMetadataInPayload`](./decorators.md#@TypeSpec.Http.includeInapplicableMetadataInPayload)
|
||||
|
||||
### Models
|
||||
|
||||
- [`Response`](./data-types.md#TypeSpec.Http.Response)
|
||||
- [`Body`](./data-types.md#TypeSpec.Http.Body)
|
||||
- [`LocationHeader`](./data-types.md#TypeSpec.Http.LocationHeader)
|
||||
- [`HeaderOptions`](./data-types.md#TypeSpec.Http.HeaderOptions)
|
||||
- [`OkResponse`](./data-types.md#TypeSpec.Http.OkResponse)
|
||||
- [`CreatedResponse`](./data-types.md#TypeSpec.Http.CreatedResponse)
|
||||
- [`AcceptedResponse`](./data-types.md#TypeSpec.Http.AcceptedResponse)
|
||||
- [`NoContentResponse`](./data-types.md#TypeSpec.Http.NoContentResponse)
|
||||
- [`MovedResponse`](./data-types.md#TypeSpec.Http.MovedResponse)
|
||||
- [`NotModifiedResponse`](./data-types.md#TypeSpec.Http.NotModifiedResponse)
|
||||
- [`BadRequestResponse`](./data-types.md#TypeSpec.Http.BadRequestResponse)
|
||||
- [`UnauthorizedResponse`](./data-types.md#TypeSpec.Http.UnauthorizedResponse)
|
||||
- [`ForbiddenResponse`](./data-types.md#TypeSpec.Http.ForbiddenResponse)
|
||||
- [`NotFoundResponse`](./data-types.md#TypeSpec.Http.NotFoundResponse)
|
||||
- [`ConflictResponse`](./data-types.md#TypeSpec.Http.ConflictResponse)
|
||||
- [`PlainData`](./data-types.md#TypeSpec.Http.PlainData)
|
||||
- [`QueryOptions`](./data-types.md#TypeSpec.Http.QueryOptions)
|
||||
- [`BasicAuth`](./data-types.md#TypeSpec.Http.BasicAuth)
|
||||
- [`BearerAuth`](./data-types.md#TypeSpec.Http.BearerAuth)
|
||||
- [`ApiKeyAuth`](./data-types.md#TypeSpec.Http.ApiKeyAuth)
|
||||
- [`OAuth2Auth`](./data-types.md#TypeSpec.Http.OAuth2Auth)
|
||||
- [`AuthorizationCodeFlow`](./data-types.md#TypeSpec.Http.AuthorizationCodeFlow)
|
||||
- [`ImplicitFlow`](./data-types.md#TypeSpec.Http.ImplicitFlow)
|
||||
- [`PasswordFlow`](./data-types.md#TypeSpec.Http.PasswordFlow)
|
||||
- [`ClientCredentialsFlow`](./data-types.md#TypeSpec.Http.ClientCredentialsFlow)
|
||||
|
||||
## TypeSpec.Rest
|
||||
|
||||
### Decorators
|
||||
|
|
|
@ -44,9 +44,7 @@ interface MyPetOps extends PetOps {
|
|||
|
||||
### Automatic route generation
|
||||
|
||||
Instead of manually specifying routes using the `@route` decorator, you automatically generate
|
||||
routes from operation parameters by applying the `@autoRoute` decorator to an operation, namespace,
|
||||
or interface containing operations.
|
||||
Instead of manually specifying routes using the `@route` decorator, you automatically generate routes from operation parameters by applying the `@autoRoute` decorator to an operation or interface containing operations.
|
||||
|
||||
For this to work, an operation's path parameters (those marked with `@path`) must also be marked with
|
||||
the `@segment` decorator to define the preceding path segment.
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"dependencies": {
|
||||
"@typespec/compiler": "latest",
|
||||
"@typespec/rest": "latest",
|
||||
"@typespec/http": "latest",
|
||||
"@typespec/versioning": "latest",
|
||||
"@typespec/openapi": "latest",
|
||||
"@typespec/openapi3": "latest"
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"reporter": ["cobertura", "json", "text"]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
require("@typespec/eslint-config-typespec/patch/modern-module-resolution");
|
||||
|
||||
module.exports = {
|
||||
plugins: ["@typespec/eslint-plugin"],
|
||||
extends: ["@typespec/eslint-config-typespec", "plugin:@typespec/eslint-plugin/recommended"],
|
||||
parserOptions: { tsconfigRootDir: __dirname },
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
timeout: 5000
|
||||
require: source-map-support/register
|
||||
spec: "dist/test/**/*.test.js"
|
||||
ignore: "dist/test/manual/**/*.js"
|
||||
|
||||
# Config for https://www.npmjs.com/package/mocha-multi-reporters
|
||||
reporterOptions: "configFile=mocha.reporter.config.json"
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "@typespec/http",
|
||||
"entries": []
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE
|
|
@ -0,0 +1,106 @@
|
|||
# TypeSpec HTTP Library
|
||||
|
||||
This package provides [TypeSpec](https://github.com/microsoft/TypeSpec) decorators, models, and interfaces to describe HTTP APIs. With fundamental models and decorators defined in TypeSpec.Http namespace, you will be able describe basic http level operations.
|
||||
|
||||
## Install
|
||||
|
||||
In your typespec project root
|
||||
|
||||
```bash
|
||||
npm install @typespec/http
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```TypeSpec
|
||||
import "@typespec/http";
|
||||
|
||||
using TypeSpec.Http;
|
||||
```
|
||||
|
||||
For more information, consult the [HTTP and REST](https://microsoft.github.io/typespec/docs/standard-library/http/) section of the TypeSpec guide.
|
||||
|
||||
## Library Tour
|
||||
|
||||
`@typespec/http` library defines of the following artifacts:
|
||||
|
||||
- [TypeSpec HTTP Library](#typespec-http-library)
|
||||
- [Install](#install)
|
||||
- [Usage](#usage)
|
||||
- [Library Tour](#library-tour)
|
||||
- [Models](#models)
|
||||
- [Decorators](#decorators)
|
||||
- [See also](#see-also)
|
||||
|
||||
## Models
|
||||
|
||||
| Model | Notes |
|
||||
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| LocationHeader | Location header |
|
||||
| Response<Status> | <Status> is numerical status code. |
|
||||
| OkResponse<T> | Response<200> with T as the response body model type. |
|
||||
| CreatedResponse | Response<201> |
|
||||
| AcceptedResponse | Response<202> |
|
||||
| NoContentResponse | Response<204> |
|
||||
| MovedResponse | Response<301> with LocationHeader for redirected URL |
|
||||
| NotModifiedResponse | Response<304> |
|
||||
| UnauthorizedResponse | Response<401> |
|
||||
| NotFoundResponse | Response<404> |
|
||||
| ConflictResponse | Response<409> |
|
||||
| PlainData<T> | Produces a new model with the same properties as T, but with @query, @header, @body, and @path decorators removed from all properties. |
|
||||
| BasicAuth | Configure `basic` authentication with @useAuth |
|
||||
| BearerAuth | Configure `bearer` authentication with @useAuth |
|
||||
| ApiKeyAuth<TLocation, TName> | Configure `apiKey` authentication with @useAuth |
|
||||
| OAuth2Auth<TFlows> | Configure `oauth2` authentication with @useAuth |
|
||||
|
||||
## Decorators
|
||||
|
||||
The `@typespec/http` library defines the following decorators in `TypeSpec.Http` namespace:
|
||||
|
||||
| Declarator | Scope | Usage |
|
||||
| ----------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| @get | operations | indicating operation uses HTTP `GET` verb. |
|
||||
| @put | operations | indicating operation uses HTTP `PUT` verb. |
|
||||
| @post | operations | indicating operation uses HTTP `POST` verb. |
|
||||
| @patch | operations | indicating operation uses HTTP `PATCH` verb. |
|
||||
| @delete | operations | indicating operation uses HTTP `DEL` verb. |
|
||||
| @head | operations | indicating operation uses HTTP `HEAD` verb. |
|
||||
| @header | model properties and operation parameters | indicating the properties are request or response headers. |
|
||||
| @query | model properties and operation parameters | indicating the properties are in the request query string. |
|
||||
| @body | model properties and operation parameters | indicating the property is in request or response body. Only one allowed per model and operation. |
|
||||
| @path | model properties and operation parameters | indicating the properties are in request path. |
|
||||
| @statusCode | model properties and operation parameters | indicating the property is the return status code. Only one allowed per model. |
|
||||
| @server | namespace | Configure the server url for the service. |
|
||||
| @route | operations, namespaces, interfaces | Syntax:<br> `@route(routeString)`<br><br>Note:<br>`@route` defines the relative route URI for the target operation. The `routeString` argument should be a URI fragment that may contain one or more path parameter fields. If the namespace or interface that contains the operation is also marked with a `@route` decorator, it will be used as a prefix to the route URI of the operation. |
|
||||
| @useAuth | namespace | Configure the service authentication. |
|
||||
|
||||
## How to
|
||||
|
||||
### Specify content type
|
||||
|
||||
To specify the content type you can add a `@header contentType: <value>` in the operation parameter(For request content type) or return type(For response content type)
|
||||
|
||||
Example: return `application/png` byte body
|
||||
|
||||
```typespec
|
||||
op getPng(): {
|
||||
@header contentType: "application/png";
|
||||
@body _: bytes;
|
||||
};
|
||||
```
|
||||
|
||||
Example: expect `application/png` byte body
|
||||
|
||||
```typespec
|
||||
op getPng(@header contentType: "application/png", @body _: bytes): void;
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- [HTTP example](https://cadlplayground.z22.web.core.windows.net/?c=aW1wb3J0ICJAY2FkbC1sYW5nL3Jlc3QiOwoKQHNlcnZpY2VUaXRsZSgiV2lkZ2V0IFPGFSIpCm5hbWVzcGFjZSBEZW1vxxg7CnVzaW5nIENhZGwuSHR0cDsKCm1vZGVsIMdAewogIEBrZXkgaWQ6IHN0cmluZzsKICB3ZWlnaHQ6IGludDMyxBFjb2xvcjogInJlZCIgfCAiYmx1ZSI7Cn0KCkBlcnJvcsdWRcQMxVVjb2Rly0BtZXNzYWdlymR9CgppbnRlcmbkALLmAI3nALTFP0DkAJ1saXN0KCk6xx9bXSB8xmHEUUByb3V0ZSgid8Uccy97aWR9IinGOHJlYWQoQHBhdGjrANfJSM1GcG9zdCBjcmVhdGUoQGJvZHkgxAXIK9Y0x3pjdXN0b21HZXTId8kR6gC0yjh9Cg%3D%3D):
|
||||
- [TypeSpec Getting Started](https://github.com/microsoft/typespec#getting-started)
|
||||
- [TypeSpec Website](https://microsoft.github.io/typespec)
|
||||
|
||||
```
|
||||
|
||||
```
|
|
@ -1,4 +1,4 @@
|
|||
import "../dist/src/http/index.js";
|
||||
import "../dist/src/index.js";
|
||||
import "./http-decorators.tsp";
|
||||
import "./auth.tsp";
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"reporterEnabled": "spec, mocha-junit-reporter"
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"name": "@typespec/http",
|
||||
"version": "0.40.0",
|
||||
"author": "Microsoft Corporation",
|
||||
"description": "TypeSpec HTTP protocol binding",
|
||||
"homepage": "https://github.com/Microsoft/typespec",
|
||||
"readme": "https://github.com/Microsoft/typespec/blob/master/README.md",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Microsoft/typespec.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/Microsoft/typespec/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"typespec"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "dist/src/index.js",
|
||||
"tspMain": "lib/http.tsp",
|
||||
"exports": {
|
||||
".": "./dist/src/index.js",
|
||||
"./testing": "./dist/src/testing/index.js"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"./dist/src/index.d.ts"
|
||||
],
|
||||
"testing": [
|
||||
"./dist/src/testing/index.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf ./dist ./temp",
|
||||
"build": "tsc -p . && npm run lint-typespec-library",
|
||||
"watch": "tsc -p . --watch",
|
||||
"lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit",
|
||||
"test": "mocha",
|
||||
"test-official": "c8 mocha --forbid-only --reporter mocha-multi-reporters",
|
||||
"lint": "eslint . --ext .ts --max-warnings=0",
|
||||
"lint:fix": "eslint . --fix --ext .ts"
|
||||
},
|
||||
"files": [
|
||||
"lib/*.tsp",
|
||||
"dist/**",
|
||||
"!dist/test/**"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@typespec/compiler": "~0.40.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "~10.0.0",
|
||||
"@types/node": "~18.11.9",
|
||||
"@typespec/compiler": "~0.40.0",
|
||||
"@typespec/eslint-config-typespec": "~0.5.0",
|
||||
"@typespec/library-linter": "~0.40.0",
|
||||
"@typespec/eslint-plugin": "~0.40.0",
|
||||
"eslint": "^8.12.0",
|
||||
"mocha": "~10.1.0",
|
||||
"mocha-junit-reporter": "~2.2.0",
|
||||
"mocha-multi-reporters": "~1.5.1",
|
||||
"c8": "~7.12.0",
|
||||
"rimraf": "~3.0.2",
|
||||
"typescript": "~4.9.3"
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { createDiagnosticCollector, Diagnostic, ModelProperty, Program } from "@typespec/compiler";
|
||||
import { createDiagnostic } from "../lib.js";
|
||||
import { getHeaderFieldName } from "./decorators.js";
|
||||
import { createDiagnostic } from "./lib.js";
|
||||
|
||||
/**
|
||||
* Check if the given model property is the content type header.
|
|
@ -4,7 +4,6 @@ import {
|
|||
Diagnostic,
|
||||
DiagnosticTarget,
|
||||
getDoc,
|
||||
Interface,
|
||||
Model,
|
||||
ModelProperty,
|
||||
Namespace,
|
||||
|
@ -18,8 +17,8 @@ import {
|
|||
validateDecoratorTarget,
|
||||
validateDecoratorUniqueOnNode,
|
||||
} from "@typespec/compiler";
|
||||
import { createDiagnostic, createStateSymbol, reportDiagnostic } from "../lib.js";
|
||||
import { extractParamsFromPath } from "../utils.js";
|
||||
import { createDiagnostic, createStateSymbol, reportDiagnostic } from "./lib.js";
|
||||
import { setRoute } from "./route.js";
|
||||
import {
|
||||
AuthenticationOption,
|
||||
HeaderFieldOptions,
|
||||
|
@ -27,10 +26,9 @@ import {
|
|||
HttpVerb,
|
||||
PathParameterOptions,
|
||||
QueryParameterOptions,
|
||||
RouteOptions,
|
||||
RoutePath,
|
||||
ServiceAuthentication,
|
||||
} from "./types.js";
|
||||
import { extractParamsFromPath } from "./utils.js";
|
||||
|
||||
export const namespace = "TypeSpec.Http";
|
||||
|
||||
|
@ -587,71 +585,10 @@ export function $route(context: DecoratorContext, entity: Type, path: string, pa
|
|||
|
||||
setRoute(context, entity, {
|
||||
path,
|
||||
isReset: false,
|
||||
shared: extractSharedValue(context, parameters),
|
||||
});
|
||||
}
|
||||
|
||||
export function $routeReset(
|
||||
context: DecoratorContext,
|
||||
entity: Type,
|
||||
path: string,
|
||||
parameters?: Model
|
||||
) {
|
||||
setRoute(context, entity, {
|
||||
path,
|
||||
isReset: true,
|
||||
shared: extractSharedValue(context, parameters),
|
||||
});
|
||||
}
|
||||
|
||||
const routeOptionsKey = createStateSymbol("routeOptions");
|
||||
export function setRouteOptionsForNamespace(
|
||||
program: Program,
|
||||
namespace: Namespace,
|
||||
options: RouteOptions
|
||||
) {
|
||||
program.stateMap(routeOptionsKey).set(namespace, options);
|
||||
}
|
||||
|
||||
export function getRouteOptionsForNamespace(
|
||||
program: Program,
|
||||
namespace: Namespace
|
||||
): RouteOptions | undefined {
|
||||
return program.stateMap(routeOptionsKey).get(namespace);
|
||||
}
|
||||
|
||||
const routesKey = createStateSymbol("routes");
|
||||
function setRoute(context: DecoratorContext, entity: Type, details: RoutePath) {
|
||||
if (
|
||||
!validateDecoratorTarget(context, entity, "@route", ["Namespace", "Interface", "Operation"])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = context.program.stateMap(routesKey);
|
||||
|
||||
if (state.has(entity) && entity.kind === "Namespace") {
|
||||
const existingValue: RoutePath = state.get(entity);
|
||||
if (existingValue.path !== details.path) {
|
||||
reportDiagnostic(context.program, {
|
||||
code: "duplicate-route-decorator",
|
||||
messageId: "namespace",
|
||||
target: entity,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
state.set(entity, details);
|
||||
}
|
||||
}
|
||||
|
||||
export function getRoutePath(
|
||||
program: Program,
|
||||
entity: Namespace | Interface | Operation
|
||||
): RoutePath | undefined {
|
||||
return program.stateMap(routesKey).get(entity);
|
||||
}
|
||||
|
||||
const includeInapplicableMetadataInPayloadKey = createStateSymbol(
|
||||
"includeInapplicableMetadataInPayload"
|
||||
);
|
|
@ -6,4 +6,6 @@ export * from "./metadata.js";
|
|||
export * from "./operations.js";
|
||||
export * from "./parameters.js";
|
||||
export * from "./responses.js";
|
||||
export * from "./route.js";
|
||||
export * from "./types.js";
|
||||
export * from "./validate.js";
|
|
@ -0,0 +1,127 @@
|
|||
import { createTypeSpecLibrary, paramMessage } from "@typespec/compiler";
|
||||
|
||||
const libDefinition = {
|
||||
name: "@typespec/http",
|
||||
diagnostics: {
|
||||
"http-verb-duplicate": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`HTTP verb already applied to ${"entityName"}`,
|
||||
},
|
||||
},
|
||||
"http-verb-wrong-type": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Cannot use @${"verb"} on a ${"entityKind"}`,
|
||||
},
|
||||
},
|
||||
"missing-path-param": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Path contains parameter ${"param"} but wasn't found in given parameters`,
|
||||
},
|
||||
},
|
||||
"optional-path-param": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Path parameter '${"paramName"}' cannot be optional.`,
|
||||
},
|
||||
},
|
||||
"missing-server-param": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Server url contains parameter '${"param"}' but wasn't found in given parameters`,
|
||||
},
|
||||
},
|
||||
"duplicate-body": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: "Operation has multiple @body parameters declared",
|
||||
duplicateUnannotated:
|
||||
"Operation has multiple unannotated parameters. There can only be one representing the body",
|
||||
bodyAndUnannotated:
|
||||
"Operation has a @body and an unannotated parameter. There can only be one representing the body",
|
||||
},
|
||||
},
|
||||
"duplicate-route-decorator": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
namespace: "@route was defined twice on this namespace and has different values.",
|
||||
},
|
||||
},
|
||||
"operation-param-duplicate-type": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Param ${"paramName"} has multiple types: [${"types"}]`,
|
||||
},
|
||||
},
|
||||
"duplicate-operation": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Duplicate operation "${"operationName"}" routed at "${"verb"} ${"path"}".`,
|
||||
},
|
||||
},
|
||||
"status-code-invalid": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default:
|
||||
"statusCode value must be a numeric or string literal or union of numeric or string literals",
|
||||
value: "statusCode value must be a three digit code between 100 and 599",
|
||||
},
|
||||
},
|
||||
"content-type-string": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: "contentType parameter must be a string literal or union of string literals",
|
||||
},
|
||||
},
|
||||
"duplicate-response": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Multiple return types for content type ${"contentType"} and status code ${"statusCode"}`,
|
||||
},
|
||||
},
|
||||
"content-type-ignored": {
|
||||
severity: "warning",
|
||||
messages: {
|
||||
default: "`Content-Type` header ignored because there is no body.",
|
||||
},
|
||||
},
|
||||
"no-routes": {
|
||||
severity: "warning",
|
||||
messages: {
|
||||
default:
|
||||
"Current spec is not exposing any routes. This could be to not having the service namespace marked with @service.",
|
||||
},
|
||||
},
|
||||
"invalid-type-for-auth": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`@useAuth ${"kind"} only accept Auth model, Tuple of auth model or union of auth model.`,
|
||||
},
|
||||
},
|
||||
"shared-boolean": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: "shared parameter must be a boolean.",
|
||||
},
|
||||
},
|
||||
"write-visibility-not-supported": {
|
||||
severity: "warning",
|
||||
messages: {
|
||||
default: `@visibility("write") is not supported. Use @visibility("update"), @visibility("create") or @visibility("create", "update") as appropriate.`,
|
||||
},
|
||||
},
|
||||
"multipart-model": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: "Multipart request body must be a model.",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const httpLib = createTypeSpecLibrary(libDefinition);
|
||||
const { reportDiagnostic, createDiagnostic, createStateSymbol } = httpLib;
|
||||
|
||||
export { httpLib, reportDiagnostic, createDiagnostic, createStateSymbol };
|
|
@ -15,10 +15,9 @@ import {
|
|||
Program,
|
||||
SyntaxKind,
|
||||
} from "@typespec/compiler";
|
||||
import { createDiagnostic, reportDiagnostic } from "../lib.js";
|
||||
import { getRoutePath } from "./decorators.js";
|
||||
import { createDiagnostic, reportDiagnostic } from "./lib.js";
|
||||
import { getResponsesForOperation } from "./responses.js";
|
||||
import { resolvePathAndParameters } from "./route.js";
|
||||
import { isSharedRoute, resolvePathAndParameters } from "./route.js";
|
||||
import {
|
||||
HttpOperation,
|
||||
HttpService,
|
||||
|
@ -140,8 +139,7 @@ export function validateRouteUnique(
|
|||
if (operation.overloading !== undefined && isOverloadSameEndpoint(operation as any)) {
|
||||
continue;
|
||||
}
|
||||
const pathShared = getRoutePath(program, operation.operation)?.shared ?? false;
|
||||
if (pathShared) {
|
||||
if (isSharedRoute(program, operation.operation)) {
|
||||
continue;
|
||||
}
|
||||
let map = grouped.get(path);
|
|
@ -7,8 +7,6 @@ import {
|
|||
Program,
|
||||
Type,
|
||||
} from "@typespec/compiler";
|
||||
import { createDiagnostic } from "../lib.js";
|
||||
import { getAction, getCollectionAction, getResourceOperation } from "../rest.js";
|
||||
import { getContentTypes, isContentTypeHeader } from "./content-types.js";
|
||||
import {
|
||||
getHeaderFieldOptions,
|
||||
|
@ -17,6 +15,7 @@ import {
|
|||
getQueryParamOptions,
|
||||
isBody,
|
||||
} from "./decorators.js";
|
||||
import { createDiagnostic } from "./lib.js";
|
||||
import { gatherMetadata, getRequestVisibility, isMetadata } from "./metadata.js";
|
||||
import {
|
||||
HttpOperation,
|
||||
|
@ -24,26 +23,24 @@ import {
|
|||
HttpOperationParameters,
|
||||
HttpOperationRequestBody,
|
||||
HttpVerb,
|
||||
OperationParameterOptions,
|
||||
} from "./types.js";
|
||||
|
||||
export function getOperationParameters(
|
||||
program: Program,
|
||||
operation: Operation,
|
||||
overloadBase?: HttpOperation,
|
||||
knownPathParamNames: string[] = []
|
||||
knownPathParamNames: string[] = [],
|
||||
options: OperationParameterOptions = {}
|
||||
): [HttpOperationParameters, readonly Diagnostic[]] {
|
||||
const verb = getExplicitVerbForOperation(program, operation);
|
||||
const verb =
|
||||
(options?.verbSelector && options.verbSelector(program, operation)) ??
|
||||
getOperationVerb(program, operation) ??
|
||||
overloadBase?.verb;
|
||||
|
||||
if (verb) {
|
||||
return getOperationParametersForVerb(program, operation, verb, knownPathParamNames);
|
||||
}
|
||||
if (overloadBase) {
|
||||
return getOperationParametersForVerb(
|
||||
program,
|
||||
operation,
|
||||
overloadBase.verb,
|
||||
knownPathParamNames
|
||||
);
|
||||
}
|
||||
|
||||
// If no verb is explicitly specified, it is POST if there is a body and
|
||||
// GET otherwise. Theoretically, it is possible to use @visibility
|
||||
|
@ -214,25 +211,3 @@ function computeHttpOperationBody(
|
|||
};
|
||||
return [body, diagnostics];
|
||||
}
|
||||
|
||||
function getExplicitVerbForOperation(program: Program, operation: Operation): HttpVerb | undefined {
|
||||
const resourceOperation = getResourceOperation(program, operation);
|
||||
const verb =
|
||||
(resourceOperation && resourceOperationToVerb[resourceOperation.operation]) ??
|
||||
getOperationVerb(program, operation) ??
|
||||
// TODO: Enable this verb choice to be customized!
|
||||
(getAction(program, operation) || getCollectionAction(program, operation) ? "post" : undefined);
|
||||
|
||||
return verb;
|
||||
}
|
||||
|
||||
// TODO: Make this overridable by libraries
|
||||
const resourceOperationToVerb: any = {
|
||||
read: "get",
|
||||
create: "post",
|
||||
createOrUpdate: "patch",
|
||||
createOrReplace: "put",
|
||||
update: "patch",
|
||||
delete: "delete",
|
||||
list: "get",
|
||||
};
|
|
@ -14,7 +14,6 @@ import {
|
|||
Type,
|
||||
walkPropertiesInherited,
|
||||
} from "@typespec/compiler";
|
||||
import { createDiagnostic } from "../lib.js";
|
||||
import { getContentTypes, isContentTypeHeader } from "./content-types.js";
|
||||
import {
|
||||
getHeaderFieldName,
|
||||
|
@ -24,6 +23,7 @@ import {
|
|||
isHeader,
|
||||
isStatusCode,
|
||||
} from "./decorators.js";
|
||||
import { createDiagnostic } from "./lib.js";
|
||||
import { gatherMetadata, isApplicableMetadata, Visibility } from "./metadata.js";
|
||||
import { HttpOperationResponse } from "./types.js";
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
import {
|
||||
createDiagnosticCollector,
|
||||
DecoratorContext,
|
||||
DiagnosticResult,
|
||||
Interface,
|
||||
Namespace,
|
||||
Operation,
|
||||
Program,
|
||||
Type,
|
||||
validateDecoratorTarget,
|
||||
} from "@typespec/compiler";
|
||||
import { createDiagnostic, createStateSymbol, reportDiagnostic } from "./lib.js";
|
||||
import { getOperationParameters } from "./parameters.js";
|
||||
import {
|
||||
HttpOperation,
|
||||
HttpOperationParameters,
|
||||
RouteOptions,
|
||||
RoutePath,
|
||||
RouteProducer,
|
||||
RouteProducerResult,
|
||||
RouteResolutionOptions,
|
||||
} from "./types.js";
|
||||
import { extractParamsFromPath } from "./utils.js";
|
||||
|
||||
// The set of allowed segment separator characters
|
||||
const AllowedSegmentSeparators = ["/", ":"];
|
||||
|
||||
function normalizeFragment(fragment: string) {
|
||||
if (fragment.length > 0 && AllowedSegmentSeparators.indexOf(fragment[0]) < 0) {
|
||||
// Insert the default separator
|
||||
fragment = `/${fragment}`;
|
||||
}
|
||||
|
||||
// Trim any trailing slash
|
||||
return fragment.replace(/\/$/g, "");
|
||||
}
|
||||
|
||||
function buildPath(pathFragments: string[]) {
|
||||
// Join all fragments with leading and trailing slashes trimmed
|
||||
const path =
|
||||
pathFragments.length === 0
|
||||
? "/"
|
||||
: pathFragments
|
||||
.map(normalizeFragment)
|
||||
.filter((x) => x !== "")
|
||||
.join("");
|
||||
|
||||
// The final path must start with a '/'
|
||||
return path.length > 0 && path[0] === "/" ? path : `/${path}`;
|
||||
}
|
||||
|
||||
export function resolvePathAndParameters(
|
||||
program: Program,
|
||||
operation: Operation,
|
||||
overloadBase: HttpOperation | undefined,
|
||||
options: RouteResolutionOptions
|
||||
): DiagnosticResult<{
|
||||
path: string;
|
||||
pathSegments: string[];
|
||||
parameters: HttpOperationParameters;
|
||||
}> {
|
||||
const diagnostics = createDiagnosticCollector();
|
||||
const { segments, parameters } = diagnostics.pipe(
|
||||
getRouteSegments(program, operation, overloadBase, options)
|
||||
);
|
||||
|
||||
// Pull out path parameters to verify what's in the path string
|
||||
const paramByName = new Set(
|
||||
parameters.parameters.filter(({ type }) => type === "path").map(({ param }) => param.name)
|
||||
);
|
||||
|
||||
// Ensure that all of the parameters defined in the route are accounted for in
|
||||
// the operation parameters
|
||||
const routeParams = segments.flatMap(extractParamsFromPath);
|
||||
for (const routeParam of routeParams) {
|
||||
if (!paramByName.has(routeParam)) {
|
||||
diagnostics.add(
|
||||
createDiagnostic({
|
||||
code: "missing-path-param",
|
||||
format: { param: routeParam },
|
||||
target: operation,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics.wrap({
|
||||
path: buildPath(segments),
|
||||
pathSegments: segments,
|
||||
parameters,
|
||||
});
|
||||
}
|
||||
|
||||
function collectSegmentsAndOptions(
|
||||
program: Program,
|
||||
source: Interface | Namespace | undefined
|
||||
): [string[], RouteOptions] {
|
||||
if (source === undefined) return [[], {}];
|
||||
|
||||
const [parentSegments, parentOptions] = collectSegmentsAndOptions(program, source.namespace);
|
||||
|
||||
const route = getRoutePath(program, source)?.path;
|
||||
const options =
|
||||
source.kind === "Namespace" ? getRouteOptionsForNamespace(program, source) ?? {} : {};
|
||||
|
||||
return [[...parentSegments, ...(route ? [route] : [])], { ...parentOptions, ...options }];
|
||||
}
|
||||
|
||||
function getRouteSegments(
|
||||
program: Program,
|
||||
operation: Operation,
|
||||
overloadBase: HttpOperation | undefined,
|
||||
options: RouteResolutionOptions
|
||||
): DiagnosticResult<RouteProducerResult> {
|
||||
const diagnostics = createDiagnosticCollector();
|
||||
const [parentSegments, parentOptions] = collectSegmentsAndOptions(
|
||||
program,
|
||||
operation.interface ?? operation.namespace
|
||||
);
|
||||
|
||||
const routeProducer = getRouteProducer(program, operation) ?? DefaultRouteProducer;
|
||||
const result = diagnostics.pipe(
|
||||
routeProducer(program, operation, parentSegments, overloadBase, {
|
||||
...parentOptions,
|
||||
...options,
|
||||
})
|
||||
);
|
||||
|
||||
return diagnostics.wrap(result);
|
||||
}
|
||||
|
||||
const externalInterfaces = createStateSymbol("externalInterfaces");
|
||||
/**
|
||||
* @deprecated DO NOT USE. For internal use only as a workaround.
|
||||
* @param program Program
|
||||
* @param target Target namespace
|
||||
* @param sourceInterface Interface that should be included in namespace.
|
||||
*/
|
||||
export function includeInterfaceRoutesInNamespace(
|
||||
program: Program,
|
||||
target: Namespace,
|
||||
sourceInterface: string
|
||||
) {
|
||||
let array = program.stateMap(externalInterfaces).get(target);
|
||||
if (array === undefined) {
|
||||
array = [];
|
||||
program.stateMap(externalInterfaces).set(target, array);
|
||||
}
|
||||
|
||||
array.push(sourceInterface);
|
||||
}
|
||||
|
||||
const routeProducerKey = createStateSymbol("routeProducer");
|
||||
|
||||
export function DefaultRouteProducer(
|
||||
program: Program,
|
||||
operation: Operation,
|
||||
parentSegments: string[],
|
||||
overloadBase: HttpOperation | undefined,
|
||||
options: RouteOptions
|
||||
): DiagnosticResult<RouteProducerResult> {
|
||||
const diagnostics = createDiagnosticCollector();
|
||||
const routePath = getRoutePath(program, operation)?.path;
|
||||
const segments =
|
||||
!routePath && overloadBase
|
||||
? overloadBase.pathSegments
|
||||
: [...parentSegments, ...(routePath ? [routePath] : [])];
|
||||
const routeParams = segments.flatMap(extractParamsFromPath);
|
||||
|
||||
const parameters: HttpOperationParameters = diagnostics.pipe(
|
||||
getOperationParameters(program, operation, overloadBase, routeParams, options.paramOptions)
|
||||
);
|
||||
|
||||
// Pull out path parameters to verify what's in the path string
|
||||
const unreferencedPathParamNames = new Set(
|
||||
parameters.parameters.filter(({ type }) => type === "path").map(({ param }) => param.name)
|
||||
);
|
||||
|
||||
// Compile the list of all route params that aren't represented in the route
|
||||
for (const routeParam of routeParams) {
|
||||
unreferencedPathParamNames.delete(routeParam);
|
||||
}
|
||||
|
||||
// Add any remaining declared path params
|
||||
for (const paramName of unreferencedPathParamNames) {
|
||||
segments.push(`{${paramName}}`);
|
||||
}
|
||||
|
||||
return diagnostics.wrap({
|
||||
segments,
|
||||
parameters,
|
||||
});
|
||||
}
|
||||
|
||||
export function setRouteProducer(
|
||||
program: Program,
|
||||
operation: Operation,
|
||||
routeProducer: RouteProducer
|
||||
): void {
|
||||
program.stateMap(routeProducerKey).set(operation, routeProducer);
|
||||
}
|
||||
|
||||
export function getRouteProducer(program: Program, operation: Operation): RouteProducer {
|
||||
return program.stateMap(routeProducerKey).get(operation);
|
||||
}
|
||||
|
||||
const routesKey = createStateSymbol("routes");
|
||||
|
||||
export function setRoute(context: DecoratorContext, entity: Type, details: RoutePath) {
|
||||
if (
|
||||
!validateDecoratorTarget(context, entity, "@route", ["Namespace", "Interface", "Operation"])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = context.program.stateMap(routesKey);
|
||||
|
||||
if (state.has(entity) && entity.kind === "Namespace") {
|
||||
const existingValue: RoutePath = state.get(entity);
|
||||
if (existingValue.path !== details.path) {
|
||||
reportDiagnostic(context.program, {
|
||||
code: "duplicate-route-decorator",
|
||||
messageId: "namespace",
|
||||
target: entity,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
state.set(entity, details);
|
||||
}
|
||||
}
|
||||
|
||||
export function isSharedRoute(program: Program, operation: Operation): boolean {
|
||||
return program.stateMap(routesKey).get(operation)?.shared;
|
||||
}
|
||||
|
||||
export function getRoutePath(
|
||||
program: Program,
|
||||
entity: Namespace | Interface | Operation
|
||||
): RoutePath | undefined {
|
||||
return program.stateMap(routesKey).get(entity);
|
||||
}
|
||||
|
||||
const routeOptionsKey = createStateSymbol("routeOptions");
|
||||
|
||||
export function setRouteOptionsForNamespace(
|
||||
program: Program,
|
||||
namespace: Namespace,
|
||||
options: RouteOptions
|
||||
) {
|
||||
program.stateMap(routeOptionsKey).set(namespace, options);
|
||||
}
|
||||
|
||||
export function getRouteOptionsForNamespace(
|
||||
program: Program,
|
||||
namespace: Namespace
|
||||
): RouteOptions | undefined {
|
||||
return program.stateMap(routeOptionsKey).get(namespace);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { resolvePath } from "@typespec/compiler";
|
||||
import { createTestLibrary, TypeSpecTestLibrary } from "@typespec/compiler/testing";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
export const HttpTestLibrary: TypeSpecTestLibrary = createTestLibrary({
|
||||
name: "@typespec/http",
|
||||
packageRoot: resolvePath(fileURLToPath(import.meta.url), "../../../../"),
|
||||
});
|
|
@ -1,9 +1,11 @@
|
|||
import {
|
||||
DiagnosticResult,
|
||||
Interface,
|
||||
ListOperationOptions,
|
||||
ModelProperty,
|
||||
Namespace,
|
||||
Operation,
|
||||
Program,
|
||||
Type,
|
||||
} from "@typespec/compiler";
|
||||
|
||||
|
@ -168,23 +170,39 @@ export interface OAuth2Scope {
|
|||
|
||||
export type OperationContainer = Namespace | Interface;
|
||||
|
||||
export interface FilteredRouteParam {
|
||||
routeParamString?: string;
|
||||
excludeFromOperationParams?: boolean;
|
||||
}
|
||||
export type OperationVerbSelector = (
|
||||
program: Program,
|
||||
operation: Operation
|
||||
) => HttpVerb | undefined;
|
||||
|
||||
export interface AutoRouteOptions {
|
||||
routeParamFilter?: (op: Operation, param: ModelProperty) => FilteredRouteParam | undefined;
|
||||
export interface OperationParameterOptions {
|
||||
verbSelector?: OperationVerbSelector;
|
||||
}
|
||||
|
||||
export interface RouteOptions {
|
||||
autoRouteOptions?: AutoRouteOptions;
|
||||
// Other options can be passed through the interface
|
||||
[prop: string]: any;
|
||||
|
||||
paramOptions?: OperationParameterOptions;
|
||||
}
|
||||
|
||||
export interface RouteResolutionOptions extends RouteOptions {
|
||||
listOptions?: ListOperationOptions;
|
||||
}
|
||||
|
||||
export interface RouteProducerResult {
|
||||
segments: string[];
|
||||
parameters: HttpOperationParameters;
|
||||
}
|
||||
|
||||
export type RouteProducer = (
|
||||
program: Program,
|
||||
operation: Operation,
|
||||
parentSegments: string[],
|
||||
overloadBase: HttpOperation | undefined,
|
||||
options: RouteOptions
|
||||
) => DiagnosticResult<RouteProducerResult>;
|
||||
|
||||
export interface HeaderFieldOptions {
|
||||
type: "header";
|
||||
name: string;
|
||||
|
@ -296,7 +314,6 @@ export interface HttpOperation {
|
|||
|
||||
export interface RoutePath {
|
||||
path: string;
|
||||
isReset: boolean;
|
||||
shared: boolean;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { Program } from "@typespec/compiler";
|
||||
import { getAllHttpServices } from "./operations.js";
|
||||
|
||||
export function $onValidate(program: Program) {
|
||||
// Pass along any diagnostics that might be returned from the HTTP library
|
||||
const [, diagnostics] = getAllHttpServices(program);
|
||||
if (diagnostics.length > 0) {
|
||||
program.reportDiagnostics(diagnostics);
|
||||
}
|
||||
}
|
|
@ -20,10 +20,10 @@ import {
|
|||
isPathParam,
|
||||
isQueryParam,
|
||||
isStatusCode,
|
||||
} from "../src/http/decorators.js";
|
||||
} from "../src/decorators.js";
|
||||
import { createHttpTestRunner } from "./test-host.js";
|
||||
|
||||
describe("rest: http decorators", () => {
|
||||
describe("http: decorators", () => {
|
||||
let runner: BasicTestRunner;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -233,11 +233,11 @@ describe("rest: http decorators", () => {
|
|||
|
||||
expectDiagnostics(diagnostics, [
|
||||
{
|
||||
code: "@typespec/rest/duplicate-operation",
|
||||
code: "@typespec/http/duplicate-operation",
|
||||
message: `Duplicate operation "test" routed at "get /test".`,
|
||||
},
|
||||
{
|
||||
code: "@typespec/rest/duplicate-operation",
|
||||
code: "@typespec/http/duplicate-operation",
|
||||
message: `Duplicate operation "test2" routed at "get /test".`,
|
||||
},
|
||||
]);
|
||||
|
@ -258,7 +258,7 @@ describe("rest: http decorators", () => {
|
|||
`);
|
||||
expectDiagnostics(diagnostics, [
|
||||
{
|
||||
code: "@typespec/rest/shared-boolean",
|
||||
code: "@typespec/http/shared-boolean",
|
||||
message: `shared parameter must be a boolean.`,
|
||||
},
|
||||
]);
|
||||
|
@ -293,7 +293,7 @@ describe("rest: http decorators", () => {
|
|||
`);
|
||||
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/rest/optional-path-param",
|
||||
code: "@typespec/http/optional-path-param",
|
||||
message: "Path parameter 'myPath' cannot be optional.",
|
||||
});
|
||||
});
|
||||
|
@ -482,7 +482,7 @@ describe("rest: http decorators", () => {
|
|||
`);
|
||||
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/rest/missing-server-param",
|
||||
code: "@typespec/http/missing-server-param",
|
||||
message: "Server url contains parameter 'name' but wasn't found in given parameters",
|
||||
});
|
||||
});
|
||||
|
@ -703,7 +703,7 @@ describe("rest: http decorators", () => {
|
|||
expectDiagnostics(diagnostics, [
|
||||
{
|
||||
severity: "warning",
|
||||
code: "@typespec/rest/write-visibility-not-supported",
|
||||
code: "@typespec/http/write-visibility-not-supported",
|
||||
},
|
||||
]);
|
||||
});
|
|
@ -1,10 +1,10 @@
|
|||
import { Operation } from "@typespec/compiler";
|
||||
import { BasicTestRunner, expectDiagnostics } from "@typespec/compiler/testing";
|
||||
import { strictEqual } from "assert";
|
||||
import { getHttpOperation, listHttpOperationsIn } from "../src/http/index.js";
|
||||
import { getHttpOperation, listHttpOperationsIn } from "../src/index.js";
|
||||
import { createHttpTestRunner } from "./test-host.js";
|
||||
|
||||
describe("rest: overloads", () => {
|
||||
describe("http: overloads", () => {
|
||||
let runner: BasicTestRunner;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -40,8 +40,7 @@ describe("rest: overloads", () => {
|
|||
@route("/uploadString")
|
||||
@test op uploadString(data: string, @header contentType: "text/plain" ): void;
|
||||
@overload(upload)
|
||||
@post
|
||||
@test op uploadBytes(data: bytes, @header contentType: "application/octet-stream"): void;
|
||||
@post @test op uploadBytes(data: bytes, @header contentType: "application/octet-stream"): void;
|
||||
`)) as { upload: Operation; uploadString: Operation; uploadBytes: Operation };
|
||||
|
||||
const [uploadHttp] = getHttpOperation(runner.program, upload);
|
||||
|
@ -96,11 +95,11 @@ describe("rest: overloads", () => {
|
|||
`);
|
||||
expectDiagnostics(diagnostics, [
|
||||
{
|
||||
code: "@typespec/rest/duplicate-operation",
|
||||
code: "@typespec/http/duplicate-operation",
|
||||
message: `Duplicate operation "otherUpload" routed at "post /upload".`,
|
||||
},
|
||||
{
|
||||
code: "@typespec/rest/duplicate-operation",
|
||||
code: "@typespec/http/duplicate-operation",
|
||||
message: `Duplicate operation "upload" routed at "post /upload".`,
|
||||
},
|
||||
]);
|
||||
|
@ -121,11 +120,11 @@ describe("rest: overloads", () => {
|
|||
`);
|
||||
expectDiagnostics(diagnostics, [
|
||||
{
|
||||
code: "@typespec/rest/duplicate-operation",
|
||||
code: "@typespec/http/duplicate-operation",
|
||||
message: `Duplicate operation "otherUploadString" routed at "post /uploadString".`,
|
||||
},
|
||||
{
|
||||
code: "@typespec/rest/duplicate-operation",
|
||||
code: "@typespec/http/duplicate-operation",
|
||||
message: `Duplicate operation "uploadString" routed at "post /uploadString".`,
|
||||
},
|
||||
]);
|
|
@ -1,20 +1,20 @@
|
|||
import { TestHost } from "@typespec/compiler/testing";
|
||||
import { ok, strictEqual } from "assert";
|
||||
import { isBody, isHeader, isPathParam, isQueryParam } from "../src/http/decorators.js";
|
||||
import { createRestTestHost } from "./test-host.js";
|
||||
import { isBody, isHeader, isPathParam, isQueryParam } from "../src/decorators.js";
|
||||
import { createHttpTestHost } from "./test-host.js";
|
||||
|
||||
describe("rest: plain data", () => {
|
||||
describe("http: plain data", () => {
|
||||
let testHost: TestHost;
|
||||
|
||||
beforeEach(async () => {
|
||||
testHost = await createRestTestHost();
|
||||
testHost = await createHttpTestHost();
|
||||
});
|
||||
|
||||
it("removes header/query/body/path", async () => {
|
||||
testHost.addTypeSpecFile(
|
||||
"main.tsp",
|
||||
`
|
||||
import "@typespec/rest";
|
||||
import "@typespec/http";
|
||||
using TypeSpec.Http;
|
||||
|
||||
@test
|
|
@ -3,7 +3,7 @@ import { expectDiagnosticEmpty, expectDiagnostics } from "@typespec/compiler/tes
|
|||
import { deepStrictEqual, ok, strictEqual } from "assert";
|
||||
import { compileOperations, getOperationsWithServiceNamespace } from "./test-host.js";
|
||||
|
||||
describe("rest: responses", () => {
|
||||
describe("http: responses", () => {
|
||||
it("issues diagnostics for duplicate body decorator", async () => {
|
||||
const [_, diagnostics] = await compileOperations(
|
||||
`
|
||||
|
@ -20,7 +20,7 @@ describe("rest: responses", () => {
|
|||
}
|
||||
`
|
||||
);
|
||||
expectDiagnostics(diagnostics, [{ code: "@typespec/rest/duplicate-body" }]);
|
||||
expectDiagnostics(diagnostics, [{ code: "@typespec/http/duplicate-body" }]);
|
||||
});
|
||||
|
||||
it("issues diagnostics for return type with duplicate status code", async () => {
|
||||
|
@ -40,7 +40,7 @@ describe("rest: responses", () => {
|
|||
`
|
||||
);
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/rest/duplicate-response",
|
||||
code: "@typespec/http/duplicate-response",
|
||||
message: "Multiple return types for content type application/json and status code 200",
|
||||
});
|
||||
});
|
||||
|
@ -70,9 +70,9 @@ describe("rest: responses", () => {
|
|||
`
|
||||
);
|
||||
expectDiagnostics(diagnostics, [
|
||||
{ code: "@typespec/rest/content-type-string" },
|
||||
{ code: "@typespec/rest/content-type-string" },
|
||||
{ code: "@typespec/rest/content-type-string" },
|
||||
{ code: "@typespec/http/content-type-string" },
|
||||
{ code: "@typespec/http/content-type-string" },
|
||||
{ code: "@typespec/http/content-type-string" },
|
||||
]);
|
||||
});
|
||||
|
|
@ -0,0 +1,548 @@
|
|||
import { expectDiagnosticEmpty, expectDiagnostics } from "@typespec/compiler/testing";
|
||||
import { deepStrictEqual, strictEqual } from "assert";
|
||||
import { HttpOperation } from "../src/index.js";
|
||||
import { compileOperations, getOperations, getRoutesFor } from "./test-host.js";
|
||||
|
||||
describe("http: routes", () => {
|
||||
// Describe how routes should be included.
|
||||
describe("route inclusion", () => {
|
||||
function expectRouteIncluded(routes: HttpOperation[], expectedRoutePaths: string[]) {
|
||||
const includedRoutes = routes.map((x) => x.path);
|
||||
deepStrictEqual(includedRoutes, expectedRoutePaths);
|
||||
}
|
||||
|
||||
describe("when there is NO service namespace", () => {
|
||||
it("operations at the document root are included", async () => {
|
||||
const routes = await getOperations(`
|
||||
@route("/one")
|
||||
@get op one(): string;
|
||||
@route("/two")
|
||||
@get op two(): string;
|
||||
`);
|
||||
|
||||
expectRouteIncluded(routes, ["/one", "/two"]);
|
||||
});
|
||||
|
||||
it("interface at the document root are included", async () => {
|
||||
const routes = await getOperations(`
|
||||
interface Foo {
|
||||
@get index(): void;
|
||||
}
|
||||
`);
|
||||
|
||||
expectRouteIncluded(routes, ["/"]);
|
||||
});
|
||||
|
||||
it("generic operation at the document root are NOT included", async () => {
|
||||
const routes = await getOperations(`
|
||||
@route("/not-included")
|
||||
@get op index<T>(): T;
|
||||
`);
|
||||
|
||||
expectRouteIncluded(routes, []);
|
||||
});
|
||||
|
||||
it("generic interface at the document root are NOT included", async () => {
|
||||
const routes = await getOperations(`
|
||||
interface Foo<T> {
|
||||
@route("/not-included")
|
||||
@get index(): T;
|
||||
}
|
||||
`);
|
||||
|
||||
expectRouteIncluded(routes, []);
|
||||
});
|
||||
|
||||
it("routes inside a namespace not marked as the service namespace aren't be included", async () => {
|
||||
const routes = await getOperations(
|
||||
`
|
||||
namespace Foo {
|
||||
@get op index(): void;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
deepStrictEqual(routes, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is a service namespace", () => {
|
||||
it("operation in the service namespace are included", async () => {
|
||||
const routes = await getOperations(
|
||||
`
|
||||
@service({title: "My Service"})
|
||||
namespace MyService;
|
||||
@get op index(): void;
|
||||
`
|
||||
);
|
||||
|
||||
expectRouteIncluded(routes, ["/"]);
|
||||
});
|
||||
|
||||
it("operation at the root of the document are NOT included", async () => {
|
||||
const routes = await getOperations(
|
||||
`
|
||||
@route("/not-included")
|
||||
@get op notIncluded(): void;
|
||||
|
||||
@service({title: "My Service"})
|
||||
namespace MyService {
|
||||
@route("/included")
|
||||
@get op included(): void;
|
||||
}
|
||||
`
|
||||
);
|
||||
expectRouteIncluded(routes, ["/included"]);
|
||||
});
|
||||
|
||||
it("interface in the service namespace are included", async () => {
|
||||
const routes = await getOperations(
|
||||
`
|
||||
@service({title: "My Service"})
|
||||
namespace MyService;
|
||||
interface Foo {
|
||||
@get index(): void;
|
||||
}`
|
||||
);
|
||||
|
||||
expectRouteIncluded(routes, ["/"]);
|
||||
});
|
||||
|
||||
it("operation in namespace in the service namespace are be included", async () => {
|
||||
const routes = await getOperations(
|
||||
`
|
||||
@service({title: "My Service"})
|
||||
namespace MyService;
|
||||
|
||||
namespace MyArea{
|
||||
@get op index(): void;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
expectRouteIncluded(routes, ["/"]);
|
||||
});
|
||||
|
||||
it("operation in a different namespace are not included", async () => {
|
||||
const routes = await getOperations(
|
||||
`
|
||||
@service({title: "My Service"})
|
||||
namespace MyService {
|
||||
@route("/included")
|
||||
@get op test(): string;
|
||||
}
|
||||
|
||||
namespace MyLib {
|
||||
@route("/not-included")
|
||||
@get op notIncluded(): void;
|
||||
}
|
||||
`
|
||||
);
|
||||
expectRouteIncluded(routes, ["/included"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("combines routes on namespaced bare operations", async () => {
|
||||
const routes = await getRoutesFor(
|
||||
`
|
||||
@route("/things")
|
||||
namespace Things {
|
||||
@get op GetThing(): string;
|
||||
|
||||
@route("/{thingId}")
|
||||
@put op CreateThing(@path thingId: string): string;
|
||||
|
||||
@route("/{thingId}/subthings")
|
||||
namespace Subthing {
|
||||
@get op GetSubthing(@path thingId: string): string;
|
||||
|
||||
@route("/{subthingId}")
|
||||
@post op CreateSubthing(@path thingId: string, @path subthingId: string): string;
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
deepStrictEqual(routes, [
|
||||
{ verb: "get", path: "/things", params: [] },
|
||||
{ verb: "put", path: "/things/{thingId}", params: ["thingId"] },
|
||||
{ verb: "get", path: "/things/{thingId}/subthings", params: ["thingId"] },
|
||||
{
|
||||
verb: "post",
|
||||
path: "/things/{thingId}/subthings/{subthingId}",
|
||||
params: ["thingId", "subthingId"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("combines routes between namespaces and interfaces", async () => {
|
||||
const routes = await getRoutesFor(
|
||||
`
|
||||
@route("/things")
|
||||
namespace Things {
|
||||
@get op GetThing(): string;
|
||||
|
||||
@route("/{thingId}")
|
||||
@put op CreateThing(@path thingId: string): string;
|
||||
|
||||
@route("/{thingId}/subthings")
|
||||
interface Subthing {
|
||||
@get GetSubthing(@path thingId: string): string;
|
||||
|
||||
@route("/{subthingId}")
|
||||
@post CreateSubthing(@path thingId: string, @path subthingId: string): string;
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
deepStrictEqual(routes, [
|
||||
{ verb: "get", path: "/things", params: [] },
|
||||
{ verb: "put", path: "/things/{thingId}", params: ["thingId"] },
|
||||
{ verb: "get", path: "/things/{thingId}/subthings", params: ["thingId"] },
|
||||
{
|
||||
verb: "post",
|
||||
path: "/things/{thingId}/subthings/{subthingId}",
|
||||
params: ["thingId", "subthingId"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("join empty route segments correctly", async () => {
|
||||
const routes = await getRoutesFor(
|
||||
`
|
||||
@route("")
|
||||
interface Foo {
|
||||
@get @route("") index(): {};
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
deepStrictEqual(routes, [{ verb: "get", path: "/", params: [] }]);
|
||||
});
|
||||
|
||||
it("join / route segments correctly", async () => {
|
||||
const routes = await getRoutesFor(
|
||||
`
|
||||
@route("/")
|
||||
interface Foo {
|
||||
@get @route("/") index(): {};
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
deepStrictEqual(routes, [{ verb: "get", path: "/", params: [] }]);
|
||||
});
|
||||
|
||||
it("always produces a route starting with /", async () => {
|
||||
const routes = await getRoutesFor(
|
||||
`
|
||||
@get
|
||||
@route(":action")
|
||||
op colonRoute(): {};
|
||||
`
|
||||
);
|
||||
|
||||
deepStrictEqual(routes, [{ verb: "get", path: "/:action", params: [] }]);
|
||||
});
|
||||
|
||||
it("defaults to POST when operation has a body but didn't specify the verb", async () => {
|
||||
const routes = await getRoutesFor(`
|
||||
@route("/test")
|
||||
op get(@body body: string): string;
|
||||
`);
|
||||
|
||||
deepStrictEqual(routes, [
|
||||
{
|
||||
verb: "post",
|
||||
path: "/test",
|
||||
params: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("emit diagnostics if 2 operation have the same path and verb", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
op get1(): string;
|
||||
|
||||
@route("/test")
|
||||
op get2(): string;
|
||||
`);
|
||||
|
||||
// Has one diagnostic per duplicate operation
|
||||
strictEqual(diagnostics.length, 2);
|
||||
strictEqual(diagnostics[0].code, "@typespec/http/duplicate-operation");
|
||||
strictEqual(diagnostics[0].message, `Duplicate operation "get1" routed at "get /test".`);
|
||||
strictEqual(diagnostics[1].code, "@typespec/http/duplicate-operation");
|
||||
strictEqual(diagnostics[1].message, `Duplicate operation "get2" routed at "get /test".`);
|
||||
});
|
||||
|
||||
describe("operation parameters", () => {
|
||||
it("emit diagnostic for parameters with multiple http request annotations", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@get op get(@body body: string, @path @query multiParam: string): string;
|
||||
`);
|
||||
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/http/operation-param-duplicate-type",
|
||||
message: "Param multiParam has multiple types: [query, path]",
|
||||
});
|
||||
});
|
||||
|
||||
it("emit diagnostic when there is an unannotated parameter and a @body param", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@get op get(param1: string, @body param2: string): string;
|
||||
`);
|
||||
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/http/duplicate-body",
|
||||
message:
|
||||
"Operation has a @body and an unannotated parameter. There can only be one representing the body",
|
||||
});
|
||||
});
|
||||
|
||||
it("emit diagnostic when there are multiple @body param", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@get op get(@query select: string, @body param1: string, @body param2: string): string;
|
||||
`);
|
||||
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/http/duplicate-body",
|
||||
message: "Operation has multiple @body parameters declared",
|
||||
});
|
||||
});
|
||||
|
||||
it("emit error if using multipart/form-data contentType parameter with a body not being a model", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@get op get(@header contentType: "multipart/form-data", @body body: string | int32): string;
|
||||
`);
|
||||
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/http/multipart-model",
|
||||
message: "Multipart request body must be a model.",
|
||||
});
|
||||
});
|
||||
|
||||
it("emit warning if using contentType parameter without a body", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@get op get(@header contentType: "image/png"): string;
|
||||
`);
|
||||
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/http/content-type-ignored",
|
||||
message: "`Content-Type` header ignored because there is no body.",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolve body when defined with @body", async () => {
|
||||
const [routes, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@get op get(@query select: string, @body bodyParam: string): string;
|
||||
`);
|
||||
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
deepStrictEqual(routes, [
|
||||
{
|
||||
verb: "get",
|
||||
path: "/test",
|
||||
params: { params: [{ type: "query", name: "select" }], body: "bodyParam" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves single unannotated parameter as request body", async () => {
|
||||
const [routes, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@get op get(@query select: string, unannotatedBodyParam: string): string;
|
||||
`);
|
||||
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
deepStrictEqual(routes, [
|
||||
{
|
||||
verb: "get",
|
||||
path: "/test",
|
||||
params: {
|
||||
params: [{ type: "query", name: "select" }],
|
||||
body: ["unannotatedBodyParam"],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves multiple unannotated parameters as request body", async () => {
|
||||
const [routes, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@get op get(
|
||||
@query select: string,
|
||||
unannotatedBodyParam1: string,
|
||||
unannotatedBodyParam2: string): string;
|
||||
`);
|
||||
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
deepStrictEqual(routes, [
|
||||
{
|
||||
verb: "get",
|
||||
path: "/test",
|
||||
params: {
|
||||
params: [{ type: "query", name: "select" }],
|
||||
body: ["unannotatedBodyParam1", "unannotatedBodyParam2"],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves unannotated path parameters that are included in the route path", async () => {
|
||||
const [routes, diagnostics] = await compileOperations(`
|
||||
@route("/test/{name}/sub/{foo}")
|
||||
@get op get(
|
||||
name: string,
|
||||
foo: string
|
||||
): string;
|
||||
|
||||
@route("/nested/{name}")
|
||||
namespace A {
|
||||
@route("sub")
|
||||
namespace B {
|
||||
@route("{bar}")
|
||||
@get op get(
|
||||
name: string,
|
||||
bar: string
|
||||
): string;
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
deepStrictEqual(routes, [
|
||||
{
|
||||
verb: "get",
|
||||
path: "/test/{name}/sub/{foo}",
|
||||
params: {
|
||||
params: [
|
||||
{ type: "path", name: "name" },
|
||||
{ type: "path", name: "foo" },
|
||||
],
|
||||
body: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
verb: "get",
|
||||
path: "/nested/{name}/sub/{bar}",
|
||||
params: {
|
||||
params: [
|
||||
{ type: "path", name: "name" },
|
||||
{ type: "path", name: "bar" },
|
||||
],
|
||||
body: undefined,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("double @route", () => {
|
||||
it("emit diagnostic if specifying route twice on operation", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@route("/test")
|
||||
op get(): string;
|
||||
`);
|
||||
expectDiagnostics(diagnostics, [
|
||||
{
|
||||
code: "duplicate-decorator",
|
||||
message: "Decorator @route cannot be used twice on the same declaration.",
|
||||
},
|
||||
{
|
||||
code: "duplicate-decorator",
|
||||
message: "Decorator @route cannot be used twice on the same declaration.",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("emit diagnostic if specifying route twice on interface", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@route("/test")
|
||||
interface Foo {
|
||||
get(): string
|
||||
}
|
||||
`);
|
||||
expectDiagnostics(diagnostics, [
|
||||
{
|
||||
code: "duplicate-decorator",
|
||||
message: "Decorator @route cannot be used twice on the same declaration.",
|
||||
},
|
||||
{
|
||||
code: "duplicate-decorator",
|
||||
message: "Decorator @route cannot be used twice on the same declaration.",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("emit diagnostic if namespace have route but different values", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test1")
|
||||
namespace Foo {
|
||||
@route("/get1")
|
||||
op get1(): string;
|
||||
}
|
||||
|
||||
@route("/test2")
|
||||
namespace Foo {
|
||||
@route("/get2")
|
||||
op get2(): string;
|
||||
}
|
||||
`);
|
||||
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/http/duplicate-route-decorator",
|
||||
message: "@route was defined twice on this namespace and has different values.",
|
||||
});
|
||||
});
|
||||
|
||||
it("merge namespace if @route value is the same", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
namespace Foo {
|
||||
@route("/get1")
|
||||
op get1(): string;
|
||||
}
|
||||
|
||||
@route("/test")
|
||||
namespace Foo {
|
||||
@route("/get2")
|
||||
op get2(): string;
|
||||
}
|
||||
`);
|
||||
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
});
|
||||
});
|
||||
|
||||
it("skips templated operations", async () => {
|
||||
const routes = await getRoutesFor(
|
||||
`
|
||||
@route("/things")
|
||||
namespace Things {
|
||||
@get op GetThingBase<TResponse>(): TResponse;
|
||||
op GetThing is GetThingBase<string>;
|
||||
|
||||
@route("/{thingId}")
|
||||
@put op CreateThing(@path thingId: string): string;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
deepStrictEqual(routes, [
|
||||
{ verb: "get", path: "/things", params: [] },
|
||||
{ verb: "put", path: "/things/{thingId}", params: ["thingId"] },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,107 @@
|
|||
import { Diagnostic } from "@typespec/compiler";
|
||||
import {
|
||||
BasicTestRunner,
|
||||
createTestHost,
|
||||
createTestWrapper,
|
||||
expectDiagnosticEmpty,
|
||||
TestHost,
|
||||
} from "@typespec/compiler/testing";
|
||||
import {
|
||||
getAllHttpServices,
|
||||
HttpOperation,
|
||||
HttpOperationParameter,
|
||||
HttpVerb,
|
||||
RouteResolutionOptions,
|
||||
} from "../src/index.js";
|
||||
import { HttpTestLibrary } from "../src/testing/index.js";
|
||||
|
||||
export async function createHttpTestHost(): Promise<TestHost> {
|
||||
return createTestHost({
|
||||
libraries: [HttpTestLibrary],
|
||||
});
|
||||
}
|
||||
export async function createHttpTestRunner(): Promise<BasicTestRunner> {
|
||||
const host = await createHttpTestHost();
|
||||
return createTestWrapper(host, { autoUsings: ["TypeSpec.Http"] });
|
||||
}
|
||||
|
||||
export interface RouteDetails {
|
||||
path: string;
|
||||
verb: HttpVerb;
|
||||
params: string[];
|
||||
}
|
||||
|
||||
export async function getRoutesFor(
|
||||
code: string,
|
||||
routeOptions?: RouteResolutionOptions
|
||||
): Promise<RouteDetails[]> {
|
||||
const [routes, diagnostics] = await compileOperations(code, routeOptions);
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
return routes.map((route) => ({
|
||||
...route,
|
||||
params: route.params.params
|
||||
.map(({ type, name }) => (type === "path" ? name : undefined))
|
||||
.filter((p) => p !== undefined) as string[],
|
||||
}));
|
||||
}
|
||||
|
||||
export interface SimpleOperationDetails {
|
||||
verb: HttpVerb;
|
||||
path: string;
|
||||
params: {
|
||||
params: Array<{ name: string; type: HttpOperationParameter["type"] }>;
|
||||
/**
|
||||
* name of explicit `@body` parameter or array of unannotated parameter names that make up the body.
|
||||
*/
|
||||
body?: string | string[];
|
||||
};
|
||||
}
|
||||
|
||||
export async function compileOperations(
|
||||
code: string,
|
||||
routeOptions?: RouteResolutionOptions
|
||||
): Promise<[SimpleOperationDetails[], readonly Diagnostic[]]> {
|
||||
const [routes, diagnostics] = await getOperationsWithServiceNamespace(code, routeOptions);
|
||||
|
||||
const details = routes.map((r) => {
|
||||
return {
|
||||
verb: r.verb,
|
||||
path: r.path,
|
||||
params: {
|
||||
params: r.parameters.parameters.map(({ type, name }) => ({ type, name })),
|
||||
body:
|
||||
r.parameters.bodyParameter?.name ??
|
||||
(r.parameters.bodyType?.kind === "Model"
|
||||
? Array.from(r.parameters.bodyType.properties.keys())
|
||||
: undefined),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return [details, diagnostics];
|
||||
}
|
||||
|
||||
export async function getOperationsWithServiceNamespace(
|
||||
code: string,
|
||||
routeOptions?: RouteResolutionOptions
|
||||
): Promise<[HttpOperation[], readonly Diagnostic[]]> {
|
||||
const runner = await createHttpTestRunner();
|
||||
await runner.compileAndDiagnose(
|
||||
`@service({title: "Test Service"}) namespace TestService;
|
||||
${code}`,
|
||||
{
|
||||
noEmit: true,
|
||||
}
|
||||
);
|
||||
const [services] = getAllHttpServices(runner.program, routeOptions);
|
||||
return [services[0].operations, runner.program.diagnostics];
|
||||
}
|
||||
|
||||
export async function getOperations(code: string): Promise<HttpOperation[]> {
|
||||
const runner = await createHttpTestRunner();
|
||||
await runner.compile(code);
|
||||
const [services, diagnostics] = getAllHttpServices(runner.program);
|
||||
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
return services[0].operations;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"references": [{ "path": "../compiler/tsconfig.json" }],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
"tsBuildInfoFile": "temp/tsconfig.tsbuildinfo",
|
||||
"types": ["node", "mocha"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "test/**/*.ts"]
|
||||
}
|
|
@ -53,12 +53,14 @@
|
|||
],
|
||||
"peerDependencies": {
|
||||
"@typespec/compiler": "~0.40.0",
|
||||
"@typespec/http": "~0.40.0",
|
||||
"@typespec/rest": "~0.40.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "~10.0.0",
|
||||
"@types/node": "~18.11.9",
|
||||
"@typespec/compiler": "~0.40.0",
|
||||
"@typespec/http": "~0.40.0",
|
||||
"@typespec/rest": "~0.40.0",
|
||||
"@typespec/eslint-config-typespec": "~0.5.0",
|
||||
"@typespec/library-linter": "~0.40.0",
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
typespecTypeToJson,
|
||||
TypeSpecValue,
|
||||
} from "@typespec/compiler";
|
||||
import { http } from "@typespec/rest";
|
||||
import { setStatusCode } from "@typespec/http";
|
||||
import { createStateSymbol, reportDiagnostic } from "./lib.js";
|
||||
import { ExtensionKey } from "./types.js";
|
||||
|
||||
|
@ -82,7 +82,7 @@ function isOpenAPIExtensionKey(key: string): key is ExtensionKey {
|
|||
*/
|
||||
const defaultResponseKey = createStateSymbol("defaultResponse");
|
||||
export function $defaultResponse(context: DecoratorContext, entity: Model) {
|
||||
http.setStatusCode(context.program, entity, ["*"]);
|
||||
setStatusCode(context.program, entity, ["*"]);
|
||||
context.program.stateSet(defaultResponseKey).add(entity);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { createTestHost, createTestWrapper } from "@typespec/compiler/testing";
|
||||
import { HttpTestLibrary } from "@typespec/http/testing";
|
||||
import { RestTestLibrary } from "@typespec/rest/testing";
|
||||
import { OpenAPITestLibrary } from "../src/testing/index.js";
|
||||
|
||||
export async function createOpenAPITestHost() {
|
||||
return createTestHost({
|
||||
libraries: [RestTestLibrary, OpenAPITestLibrary],
|
||||
libraries: [HttpTestLibrary, RestTestLibrary, OpenAPITestLibrary],
|
||||
});
|
||||
}
|
||||
export async function createOpenAPITestRunner() {
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
"peerDependencies": {
|
||||
"@typespec/versioning": "~0.40.0",
|
||||
"@typespec/compiler": "~0.40.0",
|
||||
"@typespec/http": "~0.40.0",
|
||||
"@typespec/rest": "~0.40.0",
|
||||
"@typespec/openapi": "~0.40.0"
|
||||
},
|
||||
|
@ -65,6 +66,7 @@
|
|||
"@types/node": "~18.11.9",
|
||||
"@types/js-yaml": "~4.0.1",
|
||||
"@typespec/compiler": "~0.40.0",
|
||||
"@typespec/http": "~0.40.0",
|
||||
"@typespec/rest": "~0.40.0",
|
||||
"@typespec/openapi": "~0.40.0",
|
||||
"@typespec/versioning": "~0.40.0",
|
||||
|
|
|
@ -61,17 +61,7 @@ import {
|
|||
UnionVariant,
|
||||
} from "@typespec/compiler";
|
||||
|
||||
import {
|
||||
checkDuplicateTypeName,
|
||||
getExtensions,
|
||||
getExternalDocs,
|
||||
getOpenAPITypeName,
|
||||
getParameterKey,
|
||||
isReadonlyProperty,
|
||||
resolveOperationId,
|
||||
shouldInline,
|
||||
} from "@typespec/openapi";
|
||||
import { http } from "@typespec/rest";
|
||||
import * as http from "@typespec/http";
|
||||
import {
|
||||
createMetadataInfo,
|
||||
getAuthentication,
|
||||
|
@ -90,7 +80,17 @@ import {
|
|||
reportIfNoRoutes,
|
||||
ServiceAuthentication,
|
||||
Visibility,
|
||||
} from "@typespec/rest/http";
|
||||
} from "@typespec/http";
|
||||
import {
|
||||
checkDuplicateTypeName,
|
||||
getExtensions,
|
||||
getExternalDocs,
|
||||
getOpenAPITypeName,
|
||||
getParameterKey,
|
||||
isReadonlyProperty,
|
||||
resolveOperationId,
|
||||
shouldInline,
|
||||
} from "@typespec/openapi";
|
||||
import { buildVersionProjections } from "@typespec/versioning";
|
||||
import yaml from "js-yaml";
|
||||
import { getOneOf, getRef } from "./decorators.js";
|
||||
|
|
|
@ -20,7 +20,7 @@ describe("openapi3: types included", () => {
|
|||
options: { "@typespec/openapi3": { ...options, "output-file": outPath } },
|
||||
});
|
||||
|
||||
expectDiagnosticEmpty(diagnostics.filter((x) => x.code !== "@typespec/rest/no-routes"));
|
||||
expectDiagnosticEmpty(diagnostics.filter((x) => x.code !== "@typespec/http/no-routes"));
|
||||
|
||||
const content = runner.fs.get(outPath)!;
|
||||
return JSON.parse(content);
|
||||
|
|
|
@ -46,7 +46,7 @@ describe("openapi3: output file", () => {
|
|||
options: { "@typespec/openapi3": { ...options, "emitter-output-dir": outputDir } },
|
||||
});
|
||||
|
||||
expectDiagnosticEmpty(diagnostics.filter((x) => x.code !== "@typespec/rest/no-routes"));
|
||||
expectDiagnosticEmpty(diagnostics.filter((x) => x.code !== "@typespec/http/no-routes"));
|
||||
}
|
||||
|
||||
function expectOutput(
|
||||
|
|
|
@ -268,9 +268,9 @@ describe("openapi3: return types", () => {
|
|||
`
|
||||
);
|
||||
expectDiagnostics(diagnostics, [
|
||||
{ code: "@typespec/rest/status-code-invalid" },
|
||||
{ code: "@typespec/rest/status-code-invalid" },
|
||||
{ code: "@typespec/rest/status-code-invalid" },
|
||||
{ code: "@typespec/http/status-code-invalid" },
|
||||
{ code: "@typespec/http/status-code-invalid" },
|
||||
{ code: "@typespec/http/status-code-invalid" },
|
||||
]);
|
||||
ok(diagnostics[0].message.includes("must be a numeric or string literal"));
|
||||
ok(diagnostics[1].message.includes("must be a three digit code between 100 and 599"));
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
expectDiagnosticEmpty,
|
||||
resolveVirtualPath,
|
||||
} from "@typespec/compiler/testing";
|
||||
import { HttpTestLibrary } from "@typespec/http/testing";
|
||||
import { OpenAPITestLibrary } from "@typespec/openapi/testing";
|
||||
import { RestTestLibrary } from "@typespec/rest/testing";
|
||||
import { VersioningTestLibrary } from "@typespec/versioning/testing";
|
||||
|
@ -12,7 +13,13 @@ import { OpenAPI3TestLibrary } from "../src/testing/index.js";
|
|||
|
||||
export async function createOpenAPITestHost() {
|
||||
return createTestHost({
|
||||
libraries: [RestTestLibrary, VersioningTestLibrary, OpenAPITestLibrary, OpenAPI3TestLibrary],
|
||||
libraries: [
|
||||
HttpTestLibrary,
|
||||
RestTestLibrary,
|
||||
VersioningTestLibrary,
|
||||
OpenAPITestLibrary,
|
||||
OpenAPI3TestLibrary,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -21,7 +28,9 @@ export async function createOpenAPITestRunner({
|
|||
}: { withVersioning?: boolean } = {}) {
|
||||
const host = await createOpenAPITestHost();
|
||||
const importAndUsings = `
|
||||
import "@typespec/rest"; import "@typespec/openapi";
|
||||
import "@typespec/http";
|
||||
import "@typespec/rest";
|
||||
import "@typespec/openapi";
|
||||
import "@typespec/openapi3";
|
||||
${withVersioning ? `import "@typespec/versioning"` : ""};
|
||||
using TypeSpec.Rest;
|
||||
|
@ -47,7 +56,7 @@ export async function diagnoseOpenApiFor(code: string, options: OpenAPI3EmitterO
|
|||
emit: ["@typespec/openapi3"],
|
||||
options: { "@typespec/openapi3": options as any },
|
||||
});
|
||||
return diagnostics.filter((x) => x.code !== "@typespec/rest/no-routes");
|
||||
return diagnostics.filter((x) => x.code !== "@typespec/http/no-routes");
|
||||
}
|
||||
|
||||
export async function openApiFor(
|
||||
|
@ -59,7 +68,7 @@ export async function openApiFor(
|
|||
const outPath = resolveVirtualPath("openapi.json");
|
||||
host.addTypeSpecFile(
|
||||
"./main.tsp",
|
||||
`import "@typespec/rest"; import "@typespec/openapi"; import "@typespec/openapi3"; ${
|
||||
`import "@typespec/http"; import "@typespec/rest"; import "@typespec/openapi"; import "@typespec/openapi3"; ${
|
||||
versions ? `import "@typespec/versioning"; using TypeSpec.Versioning;` : ""
|
||||
}using TypeSpec.Rest;using TypeSpec.Http;using OpenAPI;${code}`
|
||||
);
|
||||
|
@ -68,7 +77,7 @@ export async function openApiFor(
|
|||
emit: ["@typespec/openapi3"],
|
||||
options: { "@typespec/openapi3": { ...options, "output-file": outPath } },
|
||||
});
|
||||
expectDiagnosticEmpty(diagnostics.filter((x) => x.code !== "@typespec/rest/no-routes"));
|
||||
expectDiagnosticEmpty(diagnostics.filter((x) => x.code !== "@typespec/http/no-routes"));
|
||||
|
||||
if (!versions) {
|
||||
return JSON.parse(host.fs.get(outPath)!);
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
"dependencies": {
|
||||
"@typespec/versioning": "~0.40.0",
|
||||
"@typespec/compiler": "~0.40.0",
|
||||
"@typespec/http": "~0.40.0",
|
||||
"@typespec/rest": "~0.40.0",
|
||||
"@typespec/openapi3": "~0.40.0",
|
||||
"@typespec/openapi": "~0.40.0",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import "@typespec/rest";
|
||||
import "@typespec/http";
|
||||
|
||||
using TypeSpec.Http;
|
||||
@service({
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import "@typespec/http";
|
||||
import "@typespec/rest";
|
||||
|
||||
@service({
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import "@typespec/http";
|
||||
import "@typespec/rest";
|
||||
import "@typespec/openapi3";
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import "@typespec/http";
|
||||
import "@typespec/rest";
|
||||
import "@typespec/versioning";
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ const config = definePlaygroundViteConfig({
|
|||
defaultEmitter: "@typespec/openapi3",
|
||||
libraries: [
|
||||
"@typespec/compiler",
|
||||
"@typespec/http",
|
||||
"@typespec/rest",
|
||||
"@typespec/openapi",
|
||||
"@typespec/versioning",
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# TypeSpec HTTP/Rest Library
|
||||
# TypeSpec REST Library
|
||||
|
||||
This package provides [TypeSpec](https://github.com/microsoft/TypeSpec) decorators, models, and interfaces to describe HTTP and REST API. With fundamental models and decorators defined in TypeSpec.Http namespace, you will be able describe basic http level operations. TypeSpec.Rest namespace adds additional predefined models and interfaces. These building blocks make defining REST resources and operations based on standard patterns extremely simple.
|
||||
This package provides [TypeSpec](https://github.com/microsoft/TypeSpec) decorators, models, and interfaces to describe APIs using the [REST style](https://en.wikipedia.org/wiki/Representational_state_transfer). These building blocks make defining REST resources and operations based on standard patterns extremely simple.
|
||||
|
||||
## Install
|
||||
|
||||
In your typespec project root
|
||||
In your TypeSpec project root
|
||||
|
||||
```bash
|
||||
npm install @typespec/rest
|
||||
|
@ -15,11 +15,10 @@ npm install @typespec/rest
|
|||
```TypeSpec
|
||||
import "@typespec/rest";
|
||||
|
||||
using TypeSpec.Http;
|
||||
using TypeSpec.Rest;
|
||||
```
|
||||
|
||||
See [Http and rest](https://microsoft.github.io/typespec/docs/standard-library/http/).
|
||||
See [Http and rest](https://microsoft.github.io/typespec/docs/standard-library/rest/).
|
||||
|
||||
## Library Tour
|
||||
|
||||
|
@ -36,64 +35,19 @@ See [Http and rest](https://microsoft.github.io/typespec/docs/standard-library/h
|
|||
|
||||
## Models
|
||||
|
||||
- ### HTTP namespace
|
||||
|
||||
| Model | Notes |
|
||||
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| LocationHeader | Location header |
|
||||
| Response<Status> | <Status> is numerical status code. |
|
||||
| OkResponse<T> | Response<200> with T as the response body model type. |
|
||||
| CreatedResponse | Response<201> |
|
||||
| AcceptedResponse | Response<202> |
|
||||
| NoContentResponse | Response<204> |
|
||||
| MovedResponse | Response<301> with LocationHeader for redirected URL |
|
||||
| NotModifiedResponse | Response<304> |
|
||||
| UnauthorizedResponse | Response<401> |
|
||||
| NotFoundResponse | Response<404> |
|
||||
| ConflictResponse | Response<409> |
|
||||
| PlainData<T> | Produces a new model with the same properties as T, but with @query, @header, @body, and @path decorators removed from all properties. |
|
||||
| BasicAuth | Configure `basic` authentication with @useAuth |
|
||||
| BearerAuth | Configure `bearer` authentication with @useAuth |
|
||||
| ApiKeyAuth<TLocation, TName> | Configure `apiKey` authentication with @useAuth |
|
||||
| OAuth2Auth<TFlows> | Configure `oauth2` authentication with @useAuth |
|
||||
|
||||
- ### REST namespace
|
||||
| Model | Notes |
|
||||
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
|
||||
| KeysOf<T> | Dynamically gathers keys of the model type T. |
|
||||
| Page<T> | A model defines page of T which includes an array of T and optional next link. |
|
||||
| ParentKeysOf<T> | Dynamically gathers parent keys of the model type T, which are referenced with `@parentResource` decorator. |
|
||||
| ResourceError | The default error response for resource operations that includes <br> `code: int32` and `message string`. |
|
||||
| ResourceParameters<TResource> | Represents operation parameters for resource TResource. Default to KeysOf<T>. |
|
||||
| ResourceCollectionParameters<TResource> | Represents collection operation parameters for resource TResource. Default to ParentKeysOf<T> |
|
||||
| ResourceCreatedResponse<T> | Resource create operation completed successfully. |
|
||||
| ResourceDeletedResponse | Resource deleted successfully. |
|
||||
| Model | Notes |
|
||||
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
|
||||
| KeysOf<T> | Dynamically gathers keys of the model type T. |
|
||||
| Page<T> | A model defines page of T which includes an array of T and optional next link. |
|
||||
| ParentKeysOf<T> | Dynamically gathers parent keys of the model type T, which are referenced with `@parentResource` decorator. |
|
||||
| ResourceError | The default error response for resource operations that includes <br> `code: int32` and `message string`. |
|
||||
| ResourceParameters<TResource> | Represents operation parameters for resource TResource. Default to KeysOf<T>. |
|
||||
| ResourceCollectionParameters<TResource> | Represents collection operation parameters for resource TResource. Default to ParentKeysOf<T> |
|
||||
| ResourceCreatedResponse<T> | Resource create operation completed successfully. |
|
||||
| ResourceDeletedResponse | Resource deleted successfully. |
|
||||
|
||||
## Decorators
|
||||
|
||||
- ### HTTP namespace
|
||||
|
||||
The `@typespec/rest` library defines the following decorators in `TypeSpec.Http` namespace:
|
||||
|
||||
| Declarator | Scope | Usage |
|
||||
| ----------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| @get | operations | indicating operation uses HTTP `GET` verb. |
|
||||
| @put | operations | indicating operation uses HTTP `PUT` verb. |
|
||||
| @post | operations | indicating operation uses HTTP `POST` verb. |
|
||||
| @patch | operations | indicating operation uses HTTP `PATCH` verb. |
|
||||
| @delete | operations | indicating operation uses HTTP `DEL` verb. |
|
||||
| @head | operations | indicating operation uses HTTP `HEAD` verb. |
|
||||
| @header | model properties and operation parameters | indicating the properties are request or response headers. |
|
||||
| @query | model properties and operation parameters | indicating the properties are in the request query string. |
|
||||
| @body | model properties and operation parameters | indicating the property is in request or response body. Only one allowed per model and operation. |
|
||||
| @path | model properties and operation parameters | indicating the properties are in request path. |
|
||||
| @statusCode | model properties and operation parameters | indicating the property is the return status code. Only one allowed per model. |
|
||||
| @server | namespace | Configure the server url for the service. |
|
||||
| @route | operations, namespaces, interfaces | Syntax:<br> `@route(routeString)`<br><br>Note:<br>`@route` defines the relative route URI for the target operation. The `routeString` argument should be a URI fragment that may contain one or more path parameter fields. If the namespace or interface that contains the operation is also marked with a `@route` decorator, it will be used as a prefix to the route URI of the operation. |
|
||||
| @useAuth | namespace | Configure the service authentication. |
|
||||
|
||||
- ### REST namespace
|
||||
|
||||
The `@typespec/rest` library defines the following decorators in `TypeSpec.Rest` namespace:
|
||||
|
||||
| Declarator | Scope | Syntax |
|
||||
|
@ -115,10 +69,6 @@ The `@typespec/rest` library defines the following decorators in `TypeSpec.Rest`
|
|||
|
||||
## Interfaces
|
||||
|
||||
- ### HTTP namespace
|
||||
None
|
||||
- ### REST namespace
|
||||
|
||||
These standard interfaces defines resource operations in basic building blocks that you can expose on the resources. You can use `extends` to compose the operations to meet the exact needs of your resource APIs.
|
||||
|
||||
For example, for below `Widget` model
|
||||
|
@ -173,30 +123,8 @@ interface WidgetService
|
|||
| ExtensionResourceCollectionOperations<TExtension, TResource, TError> | Combines extension resource POST + LIST operations. |
|
||||
| ExtensionResourceOperations<TExtension, TResource, TError> | Combines extension resource instance and collection operations. Includes GET + PATCH + DEL + POST + LIST operations. |
|
||||
|
||||
## How to
|
||||
|
||||
### Specify content type
|
||||
|
||||
To specify the content type you can add a `@header contentType: <value>` in the operation parameter(For request content type) or return type(For response content type)
|
||||
|
||||
Example: return `application/png` byte body
|
||||
|
||||
```typespec
|
||||
op getPng(): {
|
||||
@header contentType: "application/png";
|
||||
@body _: bytes;
|
||||
};
|
||||
```
|
||||
|
||||
Example: expect `application/png` byte body
|
||||
|
||||
```typespec
|
||||
op getPng(@header contentType: "application/png", @body _: bytes): void;
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- [HTTP example](https://cadlplayground.z22.web.core.windows.net/?c=aW1wb3J0ICJAY2FkbC1sYW5nL3Jlc3QiOwoKQHNlcnZpY2VUaXRsZSgiV2lkZ2V0IFPGFSIpCm5hbWVzcGFjZSBEZW1vxxg7CnVzaW5nIENhZGwuSHR0cDsKCm1vZGVsIMdAewogIEBrZXkgaWQ6IHN0cmluZzsKICB3ZWlnaHQ6IGludDMyxBFjb2xvcjogInJlZCIgfCAiYmx1ZSI7Cn0KCkBlcnJvcsdWRcQMxVVjb2Rly0BtZXNzYWdlymR9CgppbnRlcmbkALLmAI3nALTFP0DkAJ1saXN0KCk6xx9bXSB8xmHEUUByb3V0ZSgid8Uccy97aWR9IinGOHJlYWQoQHBhdGjrANfJSM1GcG9zdCBjcmVhdGUoQGJvZHkgxAXIK9Y0x3pjdXN0b21HZXTId8kR6gC0yjh9Cg%3D%3D):
|
||||
- [REST example](https://cadlplayground.z22.web.core.windows.net/?c=aW1wb3J0ICJAY2FkbC1sYW5nL3Jlc3QiOwoKQHNlcnZpY2VUaXRsZSgiV2lkZ2V0IFPGFSIpCm5hbWVzcGFjZSBEZW1vxxg7Cgp1c2luZyBDYWRsLkh0dHA7zBFSZXN0OwoKbW9kZWwgx1J7CiAgQGtleSBpZDogc3RyaW5nOwogIHdlaWdodDogaW50MzLEEWNvbG9yOiAicmVkIiB8ICJibHVlIjsKfQoKQGVycm9yx1ZFxAzFVWNvZGXLQG1lc3NhZ2XKZH0KCmludGVyZuQAxOYAjecAxiBleHRlbmRzIFJlc291cmNl5AC5xQlPcGVyYXRpb25zPMYyLMZxPsVyQOQA0EByb3V0ZSgiY3VzdG9tR2V0IikgyQwoKTrHa%2BQAgA%3D%3D):
|
||||
- [TypeSpec Getting Started](https://github.com/microsoft/typespec#getting-started)
|
||||
- [TypeSpec Website](https://microsoft.github.io/typespec)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import "./http.tsp";
|
||||
import "@typespec/http";
|
||||
import "../dist/src/index.js";
|
||||
import "../dist/src/internal-decorators.js";
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ namespace TypeSpec.Rest;
|
|||
using TypeSpec.Reflection;
|
||||
|
||||
/**
|
||||
* This namespace, interface or operation should resolve its route automatically. To be used with resource types where the route segments area defined on the models.
|
||||
* This interface or operation should resolve its route automatically. To be used with resource types where the route segments area defined on the models.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
|
@ -14,7 +14,7 @@ using TypeSpec.Reflection;
|
|||
* }
|
||||
* ```
|
||||
*/
|
||||
extern dec autoRoute(target: Namespace | Interface | Operation);
|
||||
extern dec autoRoute(target: Interface | Operation);
|
||||
|
||||
/**
|
||||
* Defines the preceding path segment for a @path parameter in auto-generated routes.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import "./http.tsp";
|
||||
import "@typespec/http";
|
||||
import "./rest-decorators.tsp";
|
||||
import "./resource.tsp";
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
"tspMain": "lib/rest.tsp",
|
||||
"exports": {
|
||||
".": "./dist/src/index.js",
|
||||
"./http": "./dist/src/http/index.js",
|
||||
"./testing": "./dist/src/testing/index.js"
|
||||
},
|
||||
"typesVersions": {
|
||||
|
@ -29,9 +28,6 @@
|
|||
"*": [
|
||||
"./dist/src/index.d.ts"
|
||||
],
|
||||
"http": [
|
||||
"./dist/src/http/index.d.ts"
|
||||
],
|
||||
"testing": [
|
||||
"./dist/src/testing/index.d.ts"
|
||||
]
|
||||
|
@ -62,6 +58,7 @@
|
|||
"@types/mocha": "~10.0.0",
|
||||
"@types/node": "~18.11.9",
|
||||
"@typespec/compiler": "~0.40.0",
|
||||
"@typespec/http": "~0.40.0",
|
||||
"@typespec/eslint-config-typespec": "~0.5.0",
|
||||
"@typespec/library-linter": "~0.40.0",
|
||||
"@typespec/eslint-plugin": "~0.40.0",
|
||||
|
|
|
@ -1,241 +0,0 @@
|
|||
import {
|
||||
createDiagnosticCollector,
|
||||
Diagnostic,
|
||||
Interface,
|
||||
Namespace,
|
||||
Operation,
|
||||
Program,
|
||||
Type,
|
||||
} from "@typespec/compiler";
|
||||
import { createDiagnostic, createStateSymbol } from "../lib.js";
|
||||
import { getActionSegment, getActionSeparator, getSegment, isAutoRoute } from "../rest.js";
|
||||
import { extractParamsFromPath } from "../utils.js";
|
||||
import { getRouteOptionsForNamespace, getRoutePath } from "./decorators.js";
|
||||
import { getOperationParameters } from "./parameters.js";
|
||||
import {
|
||||
HttpOperation,
|
||||
HttpOperationParameter,
|
||||
HttpOperationParameters,
|
||||
RouteOptions,
|
||||
RouteResolutionOptions,
|
||||
} from "./types.js";
|
||||
|
||||
// The set of allowed segment separator characters
|
||||
const AllowedSegmentSeparators = ["/", ":"];
|
||||
|
||||
function normalizeFragment(fragment: string) {
|
||||
if (fragment.length > 0 && AllowedSegmentSeparators.indexOf(fragment[0]) < 0) {
|
||||
// Insert the default separator
|
||||
fragment = `/${fragment}`;
|
||||
}
|
||||
|
||||
// Trim any trailing slash
|
||||
return fragment.replace(/\/$/g, "");
|
||||
}
|
||||
|
||||
function buildPath(pathFragments: string[]) {
|
||||
// Join all fragments with leading and trailing slashes trimmed
|
||||
const path =
|
||||
pathFragments.length === 0
|
||||
? "/"
|
||||
: pathFragments
|
||||
.map(normalizeFragment)
|
||||
.filter((x) => x !== "")
|
||||
.join("");
|
||||
|
||||
// The final path must start with a '/'
|
||||
return path.length > 0 && path[0] === "/" ? path : `/${path}`;
|
||||
}
|
||||
|
||||
function addActionFragment(program: Program, target: Type, pathFragments: string[]) {
|
||||
// add the action segment, if present
|
||||
const defaultSeparator = "/";
|
||||
const actionSegment = getActionSegment(program, target);
|
||||
if (actionSegment && actionSegment !== "") {
|
||||
const actionSeparator = getActionSeparator(program, target) ?? defaultSeparator;
|
||||
pathFragments.push(`${actionSeparator}${actionSegment}`);
|
||||
}
|
||||
}
|
||||
|
||||
function addSegmentFragment(program: Program, target: Type, pathFragments: string[]) {
|
||||
// Don't add the segment prefix if it is meant to be excluded
|
||||
// (empty string means exclude the segment)
|
||||
const segment = getSegment(program, target);
|
||||
|
||||
if (segment && segment !== "") {
|
||||
pathFragments.push(`/${segment}`);
|
||||
}
|
||||
}
|
||||
|
||||
function generatePathFromParameters(
|
||||
program: Program,
|
||||
operation: Operation,
|
||||
pathFragments: string[],
|
||||
parameters: HttpOperationParameters,
|
||||
options: RouteResolutionOptions
|
||||
) {
|
||||
const filteredParameters: HttpOperationParameter[] = [];
|
||||
for (const httpParam of parameters.parameters) {
|
||||
const { type, param } = httpParam;
|
||||
if (type === "path") {
|
||||
addSegmentFragment(program, param, pathFragments);
|
||||
|
||||
const filteredParam = options.autoRouteOptions?.routeParamFilter?.(operation, param);
|
||||
if (filteredParam?.routeParamString) {
|
||||
pathFragments.push(`/${filteredParam.routeParamString}`);
|
||||
|
||||
if (filteredParam?.excludeFromOperationParams === true) {
|
||||
// Skip the rest of the loop so that we don't add the parameter to the final list
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Add the path variable for the parameter
|
||||
if (param.type.kind === "String") {
|
||||
pathFragments.push(`/${param.type.value}`);
|
||||
continue; // Skip adding to the parameter list
|
||||
} else {
|
||||
pathFragments.push(`/{${param.name}}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push all usable parameters to the filtered list
|
||||
filteredParameters.push(httpParam);
|
||||
}
|
||||
|
||||
// Replace the original parameters with filtered set
|
||||
parameters.parameters = filteredParameters;
|
||||
|
||||
// Add the operation's own segment if present
|
||||
addSegmentFragment(program, operation, pathFragments);
|
||||
|
||||
// Add the operation's action segment if present
|
||||
addActionFragment(program, operation, pathFragments);
|
||||
}
|
||||
|
||||
export function resolvePathAndParameters(
|
||||
program: Program,
|
||||
operation: Operation,
|
||||
overloadBase: HttpOperation | undefined,
|
||||
options: RouteResolutionOptions
|
||||
): [
|
||||
{
|
||||
path: string;
|
||||
pathSegments: string[];
|
||||
parameters: HttpOperationParameters;
|
||||
},
|
||||
readonly Diagnostic[]
|
||||
] {
|
||||
let segments = getOperationRouteSegments(program, operation, overloadBase);
|
||||
let parameters: HttpOperationParameters;
|
||||
const diagnostics = createDiagnosticCollector();
|
||||
if (isAutoRoute(program, operation)) {
|
||||
const [parentSegments, parentOptions] = getParentSegments(program, operation);
|
||||
segments = parentSegments.length ? parentSegments : segments;
|
||||
parameters = diagnostics.pipe(getOperationParameters(program, operation));
|
||||
|
||||
// The operation exists within an @autoRoute scope, generate the path. This
|
||||
// mutates the pathFragments and parameters lists that are passed in!
|
||||
generatePathFromParameters(program, operation, segments, parameters, {
|
||||
...parentOptions,
|
||||
...options,
|
||||
});
|
||||
} else {
|
||||
const declaredPathParams = segments.flatMap(extractParamsFromPath);
|
||||
parameters = diagnostics.pipe(
|
||||
getOperationParameters(program, operation, overloadBase, declaredPathParams)
|
||||
);
|
||||
|
||||
// Pull out path parameters to verify what's in the path string
|
||||
const paramByName = new Map(
|
||||
parameters.parameters
|
||||
.filter(({ type }) => type === "path")
|
||||
.map(({ param }) => [param.name, param])
|
||||
);
|
||||
|
||||
// For each param in the declared path parameters (e.g. /foo/{id} has one, id),
|
||||
// delete it because it doesn't need to be added to the path.
|
||||
for (const declaredParam of declaredPathParams) {
|
||||
const param = paramByName.get(declaredParam);
|
||||
if (!param) {
|
||||
diagnostics.add(
|
||||
createDiagnostic({
|
||||
code: "missing-path-param",
|
||||
format: { param: declaredParam },
|
||||
target: operation,
|
||||
})
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
paramByName.delete(declaredParam);
|
||||
}
|
||||
|
||||
// Add any remaining declared path params
|
||||
for (const param of paramByName.keys()) {
|
||||
segments.push(`{${param}}`);
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics.wrap({
|
||||
path: buildPath(segments),
|
||||
pathSegments: segments,
|
||||
parameters,
|
||||
});
|
||||
}
|
||||
|
||||
function getOperationRouteSegments(
|
||||
program: Program,
|
||||
operation: Operation,
|
||||
overloadBase: HttpOperation | undefined
|
||||
): string[] {
|
||||
if (overloadBase !== undefined && getRoutePath(program, operation) === undefined) {
|
||||
return overloadBase.pathSegments;
|
||||
} else {
|
||||
return getRouteSegments(program, operation)[0];
|
||||
}
|
||||
}
|
||||
|
||||
function getParentSegments(
|
||||
program: Program,
|
||||
target: Operation | Interface | Namespace
|
||||
): [string[], RouteOptions | undefined] {
|
||||
return "interface" in target && target.interface
|
||||
? getRouteSegments(program, target.interface)
|
||||
: target.namespace
|
||||
? getRouteSegments(program, target.namespace)
|
||||
: [[], undefined];
|
||||
}
|
||||
|
||||
function getRouteSegments(
|
||||
program: Program,
|
||||
target: Operation | Interface | Namespace
|
||||
): [string[], RouteOptions | undefined] {
|
||||
const route = getRoutePath(program, target)?.path;
|
||||
const seg = route ? [route] : [];
|
||||
const [parentSegments, parentOptions] = getParentSegments(program, target);
|
||||
const options =
|
||||
target.kind === "Namespace" ? getRouteOptionsForNamespace(program, target) : undefined;
|
||||
return [[...parentSegments, ...seg], options ?? parentOptions];
|
||||
}
|
||||
|
||||
const externalInterfaces = createStateSymbol("externalInterfaces");
|
||||
/**
|
||||
* @deprecated DO NOT USE. For internal use only as a workaround.
|
||||
* @param program Program
|
||||
* @param target Target namespace
|
||||
* @param sourceInterface Interface that should be included in namespace.
|
||||
*/
|
||||
export function includeInterfaceRoutesInNamespace(
|
||||
program: Program,
|
||||
target: Namespace,
|
||||
sourceInterface: string
|
||||
) {
|
||||
let array = program.stateMap(externalInterfaces).get(target);
|
||||
if (array === undefined) {
|
||||
array = [];
|
||||
program.stateMap(externalInterfaces).set(target, array);
|
||||
}
|
||||
|
||||
array.push(sourceInterface);
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
export const namespace = "TypeSpec.Rest";
|
||||
export * as http from "./http/index.js";
|
||||
export * from "./resource.js";
|
||||
export * from "./rest.js";
|
||||
export * from "./validate.js";
|
||||
|
|
|
@ -3,24 +3,6 @@ import { createTypeSpecLibrary, paramMessage } from "@typespec/compiler";
|
|||
const libDefinition = {
|
||||
name: "@typespec/rest",
|
||||
diagnostics: {
|
||||
"http-verb-duplicate": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`HTTP verb already applied to ${"entityName"}`,
|
||||
},
|
||||
},
|
||||
"http-verb-wrong-type": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Cannot use @${"verb"} on a ${"entityKind"}`,
|
||||
},
|
||||
},
|
||||
"operation-resource-wrong-type": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Cannot register resource operation "${"operation"}" on a ${"kind"}`,
|
||||
},
|
||||
},
|
||||
"not-key-type": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
|
@ -51,109 +33,6 @@ const libDefinition = {
|
|||
default: paramMessage`Resource type '${"resourceName"}' has a key property named '${"keyName"}' which conflicts with the key name of a parent or child resource.`,
|
||||
},
|
||||
},
|
||||
"missing-path-param": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Path contains parameter ${"param"} but wasn't found in given parameters`,
|
||||
},
|
||||
},
|
||||
"optional-path-param": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Path parameter '${"paramName"}' cannot be optional.`,
|
||||
},
|
||||
},
|
||||
"missing-server-param": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Server url contains parameter '${"param"}' but wasn't found in given parameters`,
|
||||
},
|
||||
},
|
||||
"duplicate-body": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: "Operation has multiple @body parameters declared",
|
||||
duplicateUnannotated:
|
||||
"Operation has multiple unannotated parameters. There can only be one representing the body",
|
||||
bodyAndUnannotated:
|
||||
"Operation has a @body and an unannotated parameter. There can only be one representing the body",
|
||||
},
|
||||
},
|
||||
"duplicate-route-decorator": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
namespace: "@route was defined twice on this namespace and has different values.",
|
||||
},
|
||||
},
|
||||
"operation-param-duplicate-type": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Param ${"paramName"} has multiple types: [${"types"}]`,
|
||||
},
|
||||
},
|
||||
"duplicate-operation": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Duplicate operation "${"operationName"}" routed at "${"verb"} ${"path"}".`,
|
||||
},
|
||||
},
|
||||
"status-code-invalid": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default:
|
||||
"statusCode value must be a numeric or string literal or union of numeric or string literals",
|
||||
value: "statusCode value must be a three digit code between 100 and 599",
|
||||
},
|
||||
},
|
||||
"content-type-string": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: "contentType parameter must be a string literal or union of string literals",
|
||||
},
|
||||
},
|
||||
"duplicate-response": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`Multiple return types for content type ${"contentType"} and status code ${"statusCode"}`,
|
||||
},
|
||||
},
|
||||
"content-type-ignored": {
|
||||
severity: "warning",
|
||||
messages: {
|
||||
default: "`Content-Type` header ignored because there is no body.",
|
||||
},
|
||||
},
|
||||
"no-routes": {
|
||||
severity: "warning",
|
||||
messages: {
|
||||
default:
|
||||
"Current spec is not exposing any routes. This could be to not having the service namespace marked with @service.",
|
||||
},
|
||||
},
|
||||
"invalid-type-for-auth": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: paramMessage`@useAuth ${"kind"} only accept Auth model, Tuple of auth model or union of auth model.`,
|
||||
},
|
||||
},
|
||||
"shared-boolean": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: "shared parameter must be a boolean.",
|
||||
},
|
||||
},
|
||||
"write-visibility-not-supported": {
|
||||
severity: "warning",
|
||||
messages: {
|
||||
default: `@visibility("write") is not supported. Use @visibility("update"), @visibility("create") or @visibility("create", "update") as appropriate.`,
|
||||
},
|
||||
},
|
||||
"multipart-model": {
|
||||
severity: "error",
|
||||
messages: {
|
||||
default: "Multipart request body must be a model.",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
Type,
|
||||
validateDecoratorTarget,
|
||||
} from "@typespec/compiler";
|
||||
import { $path } from "./http/decorators.js";
|
||||
import { $path } from "@typespec/http";
|
||||
import { createStateSymbol, reportDiagnostic } from "./lib.js";
|
||||
|
||||
export interface ResourceKey {
|
||||
|
|
|
@ -1,53 +1,185 @@
|
|||
import {
|
||||
$list,
|
||||
createDiagnosticCollector,
|
||||
DecoratorContext,
|
||||
DiagnosticResult,
|
||||
Interface,
|
||||
Model,
|
||||
ModelProperty,
|
||||
Namespace,
|
||||
Operation,
|
||||
Program,
|
||||
Scalar,
|
||||
setTypeSpecNamespace,
|
||||
Type,
|
||||
} from "@typespec/compiler";
|
||||
import {
|
||||
DefaultRouteProducer,
|
||||
getOperationParameters,
|
||||
getOperationVerb,
|
||||
getRoutePath,
|
||||
getRouteProducer,
|
||||
HttpOperation,
|
||||
HttpOperationParameter,
|
||||
HttpOperationParameters,
|
||||
HttpVerb,
|
||||
RouteOptions,
|
||||
RouteProducerResult,
|
||||
setRouteProducer,
|
||||
} from "@typespec/http";
|
||||
import { createStateSymbol, reportDiagnostic } from "./lib.js";
|
||||
import { getResourceTypeKey } from "./resource.js";
|
||||
|
||||
// ----------------- @autoRoute -----------------
|
||||
|
||||
function addActionFragment(program: Program, target: Type, pathFragments: string[]) {
|
||||
// add the action segment, if present
|
||||
const defaultSeparator = "/";
|
||||
const actionSegment = getActionSegment(program, target);
|
||||
if (actionSegment && actionSegment !== "") {
|
||||
const actionSeparator = getActionSeparator(program, target) ?? defaultSeparator;
|
||||
pathFragments.push(`${actionSeparator}${actionSegment}`);
|
||||
}
|
||||
}
|
||||
|
||||
function addSegmentFragment(program: Program, target: Type, pathFragments: string[]) {
|
||||
// Don't add the segment prefix if it is meant to be excluded
|
||||
// (empty string means exclude the segment)
|
||||
const segment = getSegment(program, target);
|
||||
|
||||
if (segment && segment !== "") {
|
||||
pathFragments.push(`/${segment}`);
|
||||
}
|
||||
}
|
||||
|
||||
export interface FilteredRouteParam {
|
||||
routeParamString?: string;
|
||||
excludeFromOperationParams?: boolean;
|
||||
}
|
||||
|
||||
export interface AutoRouteOptions {
|
||||
routeParamFilter?: (op: Operation, param: ModelProperty) => FilteredRouteParam | undefined;
|
||||
}
|
||||
|
||||
// TODO: Make this overridable by libraries
|
||||
const resourceOperationToVerb: any = {
|
||||
read: "get",
|
||||
create: "post",
|
||||
createOrUpdate: "patch",
|
||||
createOrReplace: "put",
|
||||
update: "patch",
|
||||
delete: "delete",
|
||||
list: "get",
|
||||
};
|
||||
|
||||
function getResourceOperationHttpVerb(
|
||||
program: Program,
|
||||
operation: Operation
|
||||
): HttpVerb | undefined {
|
||||
const resourceOperation = getResourceOperation(program, operation);
|
||||
return (
|
||||
getOperationVerb(program, operation) ??
|
||||
(resourceOperation && resourceOperationToVerb[resourceOperation.operation]) ??
|
||||
(getAction(program, operation) || getCollectionAction(program, operation) ? "post" : undefined)
|
||||
);
|
||||
}
|
||||
|
||||
function autoRouteProducer(
|
||||
program: Program,
|
||||
operation: Operation,
|
||||
parentSegments: string[],
|
||||
overloadBase: HttpOperation | undefined,
|
||||
options: RouteOptions
|
||||
): DiagnosticResult<RouteProducerResult> {
|
||||
const diagnostics = createDiagnosticCollector();
|
||||
const routePath = getRoutePath(program, operation)?.path;
|
||||
const segments = [...parentSegments, ...(routePath ? [routePath] : [])];
|
||||
const filteredParameters: HttpOperationParameter[] = [];
|
||||
const paramOptions = {
|
||||
...(options?.paramOptions ?? {}),
|
||||
verbSelector: getResourceOperationHttpVerb,
|
||||
};
|
||||
|
||||
const parameters: HttpOperationParameters = diagnostics.pipe(
|
||||
getOperationParameters(program, operation, undefined, [], paramOptions)
|
||||
);
|
||||
|
||||
for (const httpParam of parameters.parameters) {
|
||||
const { type, param } = httpParam;
|
||||
if (type === "path") {
|
||||
addSegmentFragment(program, param, segments);
|
||||
|
||||
const filteredParam = options.autoRouteOptions?.routeParamFilter?.(operation, param);
|
||||
if (filteredParam?.routeParamString) {
|
||||
segments.push(`/${filteredParam.routeParamString}`);
|
||||
|
||||
if (filteredParam?.excludeFromOperationParams === true) {
|
||||
// Skip the rest of the loop so that we don't add the parameter to the final list
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Add the path variable for the parameter
|
||||
if (param.type.kind === "String") {
|
||||
segments.push(`/${param.type.value}`);
|
||||
continue; // Skip adding to the parameter list
|
||||
} else {
|
||||
segments.push(`/{${param.name}}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push all usable parameters to the filtered list
|
||||
filteredParameters.push(httpParam);
|
||||
}
|
||||
|
||||
// Replace the original parameters with filtered set
|
||||
parameters.parameters = filteredParameters;
|
||||
|
||||
// Add the operation's own segment if present
|
||||
addSegmentFragment(program, operation, segments);
|
||||
|
||||
// Add the operation's action segment if present
|
||||
addActionFragment(program, operation, segments);
|
||||
|
||||
return diagnostics.wrap({
|
||||
segments,
|
||||
parameters: {
|
||||
...parameters,
|
||||
parameters: filteredParameters,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const autoRouteKey = createStateSymbol("autoRoute");
|
||||
|
||||
/**
|
||||
* `@autoRoute` enables automatic route generation for an operation, namespace, or interface.
|
||||
* `@autoRoute` enables automatic route generation for an operation or interface.
|
||||
*
|
||||
* When applied to an operation, it automatically generates the operation's route based on path parameter
|
||||
* metadata. When applied to a namespace or interface, it causes all operations under that scope to have
|
||||
* metadata. When applied to an interface, it causes all operations under that scope to have
|
||||
* auto-generated routes.
|
||||
*/
|
||||
export function $autoRoute(context: DecoratorContext, entity: Interface | Operation) {
|
||||
if (entity.kind === "Operation") {
|
||||
setRouteProducer(context.program, entity, autoRouteProducer);
|
||||
} else {
|
||||
for (const [_, op] of entity.operations) {
|
||||
// Instantly apply the decorator to the operation
|
||||
context.call($autoRoute, op);
|
||||
|
||||
// Manually push the decorator onto the property so that it gets applied
|
||||
// to operations which reference the operation with `is`
|
||||
op.decorators.push({
|
||||
decorator: $autoRoute,
|
||||
args: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function $autoRoute(context: DecoratorContext, entity: Namespace | Interface | Operation) {
|
||||
context.program.stateSet(autoRouteKey).add(entity);
|
||||
}
|
||||
|
||||
export function isAutoRoute(program: Program, target: Namespace | Interface | Operation): boolean {
|
||||
// Loop up through parent scopes (interface, namespace) to see if
|
||||
// @autoRoute was used anywhere
|
||||
let current: Namespace | Interface | Operation | undefined = target;
|
||||
while (current !== undefined) {
|
||||
if (program.stateSet(autoRouteKey).has(current)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Navigate up to the parent scope
|
||||
if (current.kind === "Namespace" || current.kind === "Interface") {
|
||||
current = current.namespace;
|
||||
} else if (current.kind === "Operation") {
|
||||
current = current.interface || current.namespace;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
export function isAutoRoute(program: Program, entity: Operation | Interface): boolean {
|
||||
return program.stateSet(autoRouteKey).has(entity);
|
||||
}
|
||||
|
||||
// ------------------ @segment ------------------
|
||||
|
@ -172,6 +304,31 @@ export interface ResourceOperation {
|
|||
|
||||
const resourceOperationsKey = createStateSymbol("resourceOperations");
|
||||
|
||||
function resourceRouteProducer(
|
||||
program: Program,
|
||||
operation: Operation,
|
||||
parentSegments: string[],
|
||||
overloadBase: HttpOperation | undefined,
|
||||
options: RouteOptions
|
||||
): DiagnosticResult<RouteProducerResult> {
|
||||
// NOTE: The purpose of this producer is to pass along the behavior of the
|
||||
// DefaultRouteProducer while setting the appropriate HTTP verb based on any
|
||||
// resource operation decorators that have been applied. This behavior will
|
||||
// be overridden by the `autoRouteProducer` if `autoRoute` is also applied to
|
||||
// the same operation.
|
||||
|
||||
// Set the OperationVerbSelector to pick verbs based on resource operation type
|
||||
const paramOptions = {
|
||||
...(options?.paramOptions ?? {}),
|
||||
verbSelector: getResourceOperationHttpVerb,
|
||||
};
|
||||
|
||||
return DefaultRouteProducer(program, operation, parentSegments, overloadBase, {
|
||||
...options,
|
||||
paramOptions,
|
||||
});
|
||||
}
|
||||
|
||||
export function setResourceOperation(
|
||||
context: DecoratorContext,
|
||||
entity: Operation,
|
||||
|
@ -187,6 +344,13 @@ export function setResourceOperation(
|
|||
operation,
|
||||
resourceType,
|
||||
});
|
||||
|
||||
// Set a custom RouteProducer on the operation if one hasn't already been
|
||||
// established yet. This is intended to translate lifecycle operations to
|
||||
// HTTP verbs.
|
||||
if (!getRouteProducer(context.program, entity)) {
|
||||
setRouteProducer(context.program, entity, resourceRouteProducer);
|
||||
}
|
||||
}
|
||||
|
||||
export function getResourceOperation(
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
navigateTypesInNamespace,
|
||||
Program,
|
||||
} from "@typespec/compiler";
|
||||
import { getAllHttpServices } from "./http/operations.js";
|
||||
import { reportDiagnostic } from "./lib.js";
|
||||
import { getParentResource, getResourceTypeKey, ResourceKey } from "./resource.js";
|
||||
|
||||
|
@ -65,10 +64,4 @@ export function $onValidate(program: Program) {
|
|||
// Make sure any defined resource types don't have any conflicts with parent
|
||||
// resource type key names
|
||||
checkForDuplicateResourceKeyNames(program);
|
||||
|
||||
// Pass along any diagnostics that might be returned from the HTTP library
|
||||
const [, diagnostics] = getAllHttpServices(program);
|
||||
if (diagnostics.length > 0) {
|
||||
program.reportDiagnostics(diagnostics);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -151,14 +151,14 @@ describe("rest: resources", () => {
|
|||
`
|
||||
using TypeSpec.Rest.Resource;
|
||||
|
||||
@autoRoute
|
||||
namespace Things {
|
||||
model Thing {
|
||||
@key
|
||||
@segment("things")
|
||||
thingId: string;
|
||||
}
|
||||
|
||||
@autoRoute
|
||||
interface Things {
|
||||
@post
|
||||
@collectionAction(Thing, "export1")
|
||||
op exportThing(): {};
|
||||
|
|
|
@ -1,240 +1,9 @@
|
|||
import { expectDiagnosticEmpty, expectDiagnostics } from "@typespec/compiler/testing";
|
||||
import { ModelProperty, Operation } from "@typespec/compiler";
|
||||
import { expectDiagnostics } from "@typespec/compiler/testing";
|
||||
import { deepStrictEqual, strictEqual } from "assert";
|
||||
import { HttpOperation } from "../src/http/types.js";
|
||||
import { compileOperations, getOperations, getRoutesFor } from "./test-host.js";
|
||||
|
||||
describe("rest: routes", () => {
|
||||
// Describe how routes should be included.
|
||||
describe("route inclusion", () => {
|
||||
function expectRouteIncluded(routes: HttpOperation[], expectedRoutePaths: string[]) {
|
||||
const includedRoutes = routes.map((x) => x.path);
|
||||
deepStrictEqual(includedRoutes, expectedRoutePaths);
|
||||
}
|
||||
|
||||
describe("when there is NO service namespace", () => {
|
||||
it("operations at the document root are included", async () => {
|
||||
const routes = await getOperations(`
|
||||
@route("/one")
|
||||
@get op one(): string;
|
||||
@route("/two")
|
||||
@get op two(): string;
|
||||
`);
|
||||
|
||||
expectRouteIncluded(routes, ["/one", "/two"]);
|
||||
});
|
||||
|
||||
it("interface at the document root are included", async () => {
|
||||
const routes = await getOperations(`
|
||||
interface Foo {
|
||||
@get index(): void;
|
||||
}
|
||||
`);
|
||||
|
||||
expectRouteIncluded(routes, ["/"]);
|
||||
});
|
||||
|
||||
it("generic operation at the document root are NOT included", async () => {
|
||||
const routes = await getOperations(`
|
||||
@route("/not-included")
|
||||
@get op index<T>(): T;
|
||||
`);
|
||||
|
||||
expectRouteIncluded(routes, []);
|
||||
});
|
||||
|
||||
it("generic interface at the document root are NOT included", async () => {
|
||||
const routes = await getOperations(`
|
||||
interface Foo<T> {
|
||||
@route("/not-included")
|
||||
@get index(): T;
|
||||
}
|
||||
`);
|
||||
|
||||
expectRouteIncluded(routes, []);
|
||||
});
|
||||
|
||||
it("routes inside a namespace not marked as the service namespace aren't be included", async () => {
|
||||
const routes = await getOperations(
|
||||
`
|
||||
namespace Foo {
|
||||
@get op index(): void;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
deepStrictEqual(routes, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is a service namespace", () => {
|
||||
it("operation in the service namespace are included", async () => {
|
||||
const routes = await getOperations(
|
||||
`
|
||||
@service({title: "My Service"})
|
||||
namespace MyService;
|
||||
@get op index(): void;
|
||||
`
|
||||
);
|
||||
|
||||
expectRouteIncluded(routes, ["/"]);
|
||||
});
|
||||
|
||||
it("operation at the root of the document are NOT included", async () => {
|
||||
const routes = await getOperations(
|
||||
`
|
||||
@route("/not-included")
|
||||
@get op notIncluded(): void;
|
||||
|
||||
@service({title: "My Service"})
|
||||
namespace MyService {
|
||||
@route("/included")
|
||||
@get op included(): void;
|
||||
}
|
||||
`
|
||||
);
|
||||
expectRouteIncluded(routes, ["/included"]);
|
||||
});
|
||||
|
||||
it("interface in the service namespace are included", async () => {
|
||||
const routes = await getOperations(
|
||||
`
|
||||
@service({title: "My Service"})
|
||||
namespace MyService;
|
||||
interface Foo {
|
||||
@get index(): void;
|
||||
}`
|
||||
);
|
||||
|
||||
expectRouteIncluded(routes, ["/"]);
|
||||
});
|
||||
|
||||
it("operation in namespace in the service namespace are be included", async () => {
|
||||
const routes = await getOperations(
|
||||
`
|
||||
@service({title: "My Service"})
|
||||
namespace MyService;
|
||||
|
||||
namespace MyArea{
|
||||
@get op index(): void;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
expectRouteIncluded(routes, ["/"]);
|
||||
});
|
||||
|
||||
it("operation in a different namespace are not included", async () => {
|
||||
const routes = await getOperations(
|
||||
`
|
||||
@service({title: "My Service"})
|
||||
namespace MyService {
|
||||
@route("/included")
|
||||
@get op test(): string;
|
||||
}
|
||||
|
||||
namespace MyLib {
|
||||
@route("/not-included")
|
||||
@get op notIncluded(): void;
|
||||
}
|
||||
`
|
||||
);
|
||||
expectRouteIncluded(routes, ["/included"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("combines routes on namespaced bare operations", async () => {
|
||||
const routes = await getRoutesFor(
|
||||
`
|
||||
@route("/things")
|
||||
namespace Things {
|
||||
@get op GetThing(): string;
|
||||
|
||||
@route("/{thingId}")
|
||||
@put op CreateThing(@path thingId: string): string;
|
||||
|
||||
@route("/{thingId}/subthings")
|
||||
namespace Subthing {
|
||||
@get op GetSubthing(@path thingId: string): string;
|
||||
|
||||
@route("/{subthingId}")
|
||||
@post op CreateSubthing(@path thingId: string, @path subthingId: string): string;
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
deepStrictEqual(routes, [
|
||||
{ verb: "get", path: "/things", params: [] },
|
||||
{ verb: "put", path: "/things/{thingId}", params: ["thingId"] },
|
||||
{ verb: "get", path: "/things/{thingId}/subthings", params: ["thingId"] },
|
||||
{
|
||||
verb: "post",
|
||||
path: "/things/{thingId}/subthings/{subthingId}",
|
||||
params: ["thingId", "subthingId"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("combines routes between namespaces and interfaces", async () => {
|
||||
const routes = await getRoutesFor(
|
||||
`
|
||||
@route("/things")
|
||||
namespace Things {
|
||||
@get op GetThing(): string;
|
||||
|
||||
@route("/{thingId}")
|
||||
@put op CreateThing(@path thingId: string): string;
|
||||
|
||||
@route("/{thingId}/subthings")
|
||||
interface Subthing {
|
||||
@get GetSubthing(@path thingId: string): string;
|
||||
|
||||
@route("/{subthingId}")
|
||||
@post CreateSubthing(@path thingId: string, @path subthingId: string): string;
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
deepStrictEqual(routes, [
|
||||
{ verb: "get", path: "/things", params: [] },
|
||||
{ verb: "put", path: "/things/{thingId}", params: ["thingId"] },
|
||||
{ verb: "get", path: "/things/{thingId}/subthings", params: ["thingId"] },
|
||||
{
|
||||
verb: "post",
|
||||
path: "/things/{thingId}/subthings/{subthingId}",
|
||||
params: ["thingId", "subthingId"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("join empty route segments correctly", async () => {
|
||||
const routes = await getRoutesFor(
|
||||
`
|
||||
@route("")
|
||||
interface Foo {
|
||||
@get @route("") index(): {};
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
deepStrictEqual(routes, [{ verb: "get", path: "/", params: [] }]);
|
||||
});
|
||||
|
||||
it("join / route segments correctly", async () => {
|
||||
const routes = await getRoutesFor(
|
||||
`
|
||||
@route("/")
|
||||
interface Foo {
|
||||
@get @route("/") index(): {};
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
deepStrictEqual(routes, [{ verb: "get", path: "/", params: [] }]);
|
||||
});
|
||||
|
||||
it("always produces a route starting with /", async () => {
|
||||
const routes = await getRoutesFor(
|
||||
`
|
||||
|
@ -266,7 +35,7 @@ describe("rest: routes", () => {
|
|||
}
|
||||
|
||||
@autoRoute
|
||||
namespace Things {
|
||||
interface Things {
|
||||
@get
|
||||
@action
|
||||
op ActionOne(...ThingId): string;
|
||||
|
@ -311,8 +80,8 @@ describe("rest: routes", () => {
|
|||
@autoRoute
|
||||
@get op GetThingWithParams(...KeysOf<Thing>): string;
|
||||
|
||||
@autoRoute
|
||||
namespace SubNamespace {
|
||||
@autoRoute
|
||||
@put op CreateThing(...KeysOf<Thing>): string;
|
||||
}
|
||||
|
||||
|
@ -352,7 +121,7 @@ describe("rest: routes", () => {
|
|||
}
|
||||
|
||||
@autoRoute
|
||||
namespace Things {
|
||||
interface Things {
|
||||
@get
|
||||
op WithFilteredParam(
|
||||
@path
|
||||
|
@ -377,38 +146,6 @@ describe("rest: routes", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it("defaults to POST when operation has a body but didn't specify the verb", async () => {
|
||||
const routes = await getRoutesFor(`
|
||||
@route("/test")
|
||||
op get(@body body: string): string;
|
||||
`);
|
||||
|
||||
deepStrictEqual(routes, [
|
||||
{
|
||||
verb: "post",
|
||||
path: "/test",
|
||||
params: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("emit diagnostics if 2 operation have the same path and verb", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
op get1(): string;
|
||||
|
||||
@route("/test")
|
||||
op get2(): string;
|
||||
`);
|
||||
|
||||
// Has one diagnostic per duplicate operation
|
||||
strictEqual(diagnostics.length, 2);
|
||||
strictEqual(diagnostics[0].code, "@typespec/rest/duplicate-operation");
|
||||
strictEqual(diagnostics[0].message, `Duplicate operation "get1" routed at "get /test".`);
|
||||
strictEqual(diagnostics[1].code, "@typespec/rest/duplicate-operation");
|
||||
strictEqual(diagnostics[1].message, `Duplicate operation "get2" routed at "get /test".`);
|
||||
});
|
||||
|
||||
it("emit diagnostic if passing arguments to autoroute decorators", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@autoRoute("/test") op test(): string;
|
||||
|
@ -420,253 +157,6 @@ describe("rest: routes", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("operation parameters", () => {
|
||||
it("emit diagnostic for parameters with multiple http request annotations", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@get op get(@body body: string, @path @query multiParam: string): string;
|
||||
`);
|
||||
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/rest/operation-param-duplicate-type",
|
||||
message: "Param multiParam has multiple types: [query, path]",
|
||||
});
|
||||
});
|
||||
|
||||
it("emit diagnostic when there is an unannotated parameter and a @body param", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@get op get(param1: string, @body param2: string): string;
|
||||
`);
|
||||
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/rest/duplicate-body",
|
||||
message:
|
||||
"Operation has a @body and an unannotated parameter. There can only be one representing the body",
|
||||
});
|
||||
});
|
||||
|
||||
it("emit diagnostic when there are multiple @body param", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@get op get(@query select: string, @body param1: string, @body param2: string): string;
|
||||
`);
|
||||
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/rest/duplicate-body",
|
||||
message: "Operation has multiple @body parameters declared",
|
||||
});
|
||||
});
|
||||
|
||||
it("emit error if using multipart/form-data contentType parameter with a body not being a model", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@get op get(@header contentType: "multipart/form-data", @body body: string | int32): string;
|
||||
`);
|
||||
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/rest/multipart-model",
|
||||
message: "Multipart request body must be a model.",
|
||||
});
|
||||
});
|
||||
|
||||
it("emit warning if using contentType parameter without a body", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@get op get(@header contentType: "image/png"): string;
|
||||
`);
|
||||
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/rest/content-type-ignored",
|
||||
message: "`Content-Type` header ignored because there is no body.",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolve body when defined with @body", async () => {
|
||||
const [routes, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@get op get(@query select: string, @body bodyParam: string): string;
|
||||
`);
|
||||
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
deepStrictEqual(routes, [
|
||||
{
|
||||
verb: "get",
|
||||
path: "/test",
|
||||
params: { params: [{ type: "query", name: "select" }], body: "bodyParam" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves single unannotated parameter as request body", async () => {
|
||||
const [routes, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@get op get(@query select: string, unannotatedBodyParam: string): string;
|
||||
`);
|
||||
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
deepStrictEqual(routes, [
|
||||
{
|
||||
verb: "get",
|
||||
path: "/test",
|
||||
params: {
|
||||
params: [{ type: "query", name: "select" }],
|
||||
body: ["unannotatedBodyParam"],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves multiple unannotated parameters as request body", async () => {
|
||||
const [routes, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@get op get(
|
||||
@query select: string,
|
||||
unannotatedBodyParam1: string,
|
||||
unannotatedBodyParam2: string): string;
|
||||
`);
|
||||
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
deepStrictEqual(routes, [
|
||||
{
|
||||
verb: "get",
|
||||
path: "/test",
|
||||
params: {
|
||||
params: [{ type: "query", name: "select" }],
|
||||
body: ["unannotatedBodyParam1", "unannotatedBodyParam2"],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves unannotated path parameters that are included in the route path", async () => {
|
||||
const [routes, diagnostics] = await compileOperations(`
|
||||
@route("/test/{name}/sub/{foo}")
|
||||
@get op get(
|
||||
name: string,
|
||||
foo: string
|
||||
): string;
|
||||
|
||||
@route("/nested/{name}")
|
||||
namespace A {
|
||||
@route("sub")
|
||||
namespace B {
|
||||
@route("{bar}")
|
||||
@get op get(
|
||||
name: string,
|
||||
bar: string
|
||||
): string;
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
deepStrictEqual(routes, [
|
||||
{
|
||||
verb: "get",
|
||||
path: "/test/{name}/sub/{foo}",
|
||||
params: {
|
||||
params: [
|
||||
{ type: "path", name: "name" },
|
||||
{ type: "path", name: "foo" },
|
||||
],
|
||||
body: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
verb: "get",
|
||||
path: "/nested/{name}/sub/{bar}",
|
||||
params: {
|
||||
params: [
|
||||
{ type: "path", name: "name" },
|
||||
{ type: "path", name: "bar" },
|
||||
],
|
||||
body: undefined,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("double @route", () => {
|
||||
it("emit diagnostic if specifying route twice on operation", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@route("/test")
|
||||
op get(): string;
|
||||
`);
|
||||
expectDiagnostics(diagnostics, [
|
||||
{
|
||||
code: "duplicate-decorator",
|
||||
message: "Decorator @route cannot be used twice on the same declaration.",
|
||||
},
|
||||
{
|
||||
code: "duplicate-decorator",
|
||||
message: "Decorator @route cannot be used twice on the same declaration.",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("emit diagnostic if specifying route twice on interface", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
@route("/test")
|
||||
interface Foo {
|
||||
get(): string
|
||||
}
|
||||
`);
|
||||
expectDiagnostics(diagnostics, [
|
||||
{
|
||||
code: "duplicate-decorator",
|
||||
message: "Decorator @route cannot be used twice on the same declaration.",
|
||||
},
|
||||
{
|
||||
code: "duplicate-decorator",
|
||||
message: "Decorator @route cannot be used twice on the same declaration.",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("emit diagnostic if namespace have route but different values", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test1")
|
||||
namespace Foo {
|
||||
@route("/get1")
|
||||
op get1(): string;
|
||||
}
|
||||
|
||||
@route("/test2")
|
||||
namespace Foo {
|
||||
@route("/get2")
|
||||
op get2(): string;
|
||||
}
|
||||
`);
|
||||
|
||||
expectDiagnostics(diagnostics, {
|
||||
code: "@typespec/rest/duplicate-route-decorator",
|
||||
message: "@route was defined twice on this namespace and has different values.",
|
||||
});
|
||||
});
|
||||
|
||||
it("merge namespace if @route value is the same", async () => {
|
||||
const [_, diagnostics] = await compileOperations(`
|
||||
@route("/test")
|
||||
namespace Foo {
|
||||
@route("/get1")
|
||||
op get1(): string;
|
||||
}
|
||||
|
||||
@route("/test")
|
||||
namespace Foo {
|
||||
@route("/get2")
|
||||
op get2(): string;
|
||||
}
|
||||
`);
|
||||
|
||||
expectDiagnosticEmpty(diagnostics);
|
||||
});
|
||||
});
|
||||
|
||||
describe("use of @route with @autoRoute", () => {
|
||||
it("can override library operation route in service", async () => {
|
||||
const ops = await getOperations(`
|
||||
|
@ -745,6 +235,7 @@ describe("rest: routes", () => {
|
|||
@service({title: "Test"})
|
||||
namespace Test {
|
||||
op my is Lib.action;
|
||||
|
||||
@route("my")
|
||||
op my2 is Lib.action;
|
||||
}
|
||||
|
@ -794,9 +285,9 @@ describe("rest: routes", () => {
|
|||
}
|
||||
`);
|
||||
strictEqual(ops[0].verb, "get");
|
||||
strictEqual(ops[0].path, "/{id}");
|
||||
strictEqual(ops[0].path, "/pets/{id}");
|
||||
strictEqual(ops[1].verb, "get");
|
||||
strictEqual(ops[1].path, "/my/{id}");
|
||||
strictEqual(ops[1].path, "/my/pets/{id}");
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -817,7 +308,7 @@ describe("rest: routes", () => {
|
|||
`,
|
||||
{
|
||||
autoRouteOptions: {
|
||||
routeParamFilter: (_, param) => {
|
||||
routeParamFilter: (_: Operation, param: ModelProperty) => {
|
||||
return {
|
||||
routeParamString: param.name === "subThingId" ? "bar" : "{foo}",
|
||||
excludeFromOperationParams: true,
|
||||
|
@ -834,7 +325,7 @@ describe("rest: routes", () => {
|
|||
const routes = await getRoutesFor(
|
||||
`
|
||||
@autoRoute
|
||||
namespace Things {
|
||||
interface Things {
|
||||
@action
|
||||
@actionSeparator(":")
|
||||
@put op customAction(
|
||||
|
@ -854,7 +345,7 @@ describe("rest: routes", () => {
|
|||
const routes = await getRoutesFor(
|
||||
`
|
||||
@autoRoute
|
||||
namespace Things {
|
||||
interface Things {
|
||||
@action
|
||||
@actionSeparator(":")
|
||||
@put op customAction1(
|
||||
|
@ -889,7 +380,7 @@ describe("rest: routes", () => {
|
|||
const [_, diagnostics] = await compileOperations(
|
||||
`
|
||||
@autoRoute
|
||||
namespace Things {
|
||||
interface Things {
|
||||
@action
|
||||
@actionSeparator("x")
|
||||
@put op customAction(
|
||||
|
|
|
@ -12,19 +12,15 @@ import {
|
|||
HttpOperationParameter,
|
||||
HttpVerb,
|
||||
RouteResolutionOptions,
|
||||
} from "../src/http/index.js";
|
||||
} from "@typespec/http";
|
||||
import { HttpTestLibrary } from "@typespec/http/testing";
|
||||
import { RestTestLibrary } from "../src/testing/index.js";
|
||||
|
||||
export async function createRestTestHost(): Promise<TestHost> {
|
||||
return createTestHost({
|
||||
libraries: [RestTestLibrary],
|
||||
libraries: [HttpTestLibrary, RestTestLibrary],
|
||||
});
|
||||
}
|
||||
export async function createHttpTestRunner(): Promise<BasicTestRunner> {
|
||||
const host = await createRestTestHost();
|
||||
return createTestWrapper(host, { autoUsings: ["TypeSpec.Http"] });
|
||||
}
|
||||
|
||||
export async function createRestTestRunner(): Promise<BasicTestRunner> {
|
||||
const host = await createRestTestHost();
|
||||
return createTestWrapper(host, { autoUsings: ["TypeSpec.Http", "TypeSpec.Rest"] });
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
"dependencies": {
|
||||
"@typespec/versioning": "~0.40.0",
|
||||
"@typespec/compiler": "~0.40.0",
|
||||
"@typespec/http": "~0.40.0",
|
||||
"@typespec/rest": "~0.40.0",
|
||||
"@typespec/openapi": "~0.40.0",
|
||||
"@typespec/openapi3": "~0.40.0",
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
HttpOperationResponse,
|
||||
HttpVerb,
|
||||
Visibility,
|
||||
} from "@typespec/rest/http";
|
||||
} from "@typespec/http";
|
||||
import { buildVersionProjections } from "@typespec/versioning";
|
||||
import assert from "assert";
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import "@typespec/rest";
|
||||
import "@typespec/openapi";
|
||||
|
||||
@autoRoute
|
||||
@service({
|
||||
title: "Pet Store Service",
|
||||
version: "2021-03-25",
|
||||
|
@ -72,12 +71,14 @@ interface PetCheckups
|
|||
interface PetInsurance extends SingletonResourceOperations<Insurance, Pet, PetStoreError> {}
|
||||
|
||||
interface Toys extends ResourceRead<Toy, PetStoreError> {
|
||||
@autoRoute
|
||||
@listsResource(Toy)
|
||||
list(
|
||||
...ParentKeysOf<Toy>,
|
||||
@query nameFilter: string
|
||||
): CollectionWithNextLink<Toy> | PetStoreError;
|
||||
}
|
||||
|
||||
interface ToyInsurance extends SingletonResourceOperations<Insurance, Toy, PetStoreError> {}
|
||||
|
||||
interface Checkups
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import "@typespec/http";
|
||||
import "@typespec/rest";
|
||||
|
||||
using TypeSpec.Http;
|
||||
using TypeSpec.Rest;
|
||||
|
||||
@service({
|
||||
title: "Visibility sample",
|
||||
|
|
|
@ -13,10 +13,17 @@ await generateLibraryDocs(
|
|||
join(repoRoot, "docs/compiler/reference")
|
||||
);
|
||||
|
||||
// Http
|
||||
await generateLibraryDocs(
|
||||
join(repoRoot, "packages/http"),
|
||||
["TypeSpec.Http"],
|
||||
join(repoRoot, "docs/standard-library/http/reference")
|
||||
);
|
||||
|
||||
// Rest
|
||||
await generateLibraryDocs(
|
||||
join(repoRoot, "packages/rest"),
|
||||
["TypeSpec.Http", "TypeSpec.Rest", "TypeSpec.Rest.Resource"],
|
||||
["TypeSpec.Rest", "TypeSpec.Rest.Resource"],
|
||||
join(repoRoot, "docs/standard-library/rest/reference")
|
||||
);
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"devDependencies": {
|
||||
"@typespec/ref-doc": "^0.1.0",
|
||||
"@typespec/spec": "0.1.0",
|
||||
"@typespec/http": "~0.40.0",
|
||||
"@typespec/rest": "~0.40.0",
|
||||
"@typespec/openapi": "~0.40.0",
|
||||
"@docusaurus/module-type-aliases": "^2.2.0",
|
||||
|
|
|
@ -90,6 +90,12 @@
|
|||
"reviewCategory": "production",
|
||||
"versionPolicyName": "typespec"
|
||||
},
|
||||
{
|
||||
"packageName": "@typespec/http",
|
||||
"projectFolder": "packages/http",
|
||||
"reviewCategory": "production",
|
||||
"versionPolicyName": "typespec"
|
||||
},
|
||||
{
|
||||
"packageName": "@typespec/rest",
|
||||
"projectFolder": "packages/rest",
|
||||
|
|
Загрузка…
Ссылка в новой задаче