diff --git a/.chronus/changes/add-completion-for-decorator-model-arg-2024-4-17-11-50-34.md b/.chronus/changes/add-completion-for-decorator-model-arg-2024-4-17-11-50-34.md deleted file mode 100644 index 993f7548285..00000000000 --- a/.chronus/changes/add-completion-for-decorator-model-arg-2024-4-17-11-50-34.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -changeKind: feature -packages: - - "@typespec/compiler" ---- - -Support completion for Model with extended properties - - Example - ```tsp - model Device { - name: string; - description: string; - } - - model Phone extends Device { - ┆ - } | [name] - | [description] - ``` - diff --git a/.chronus/changes/add-completion-for-decorator-model-arg-2024-4-6-16-27-17.md b/.chronus/changes/add-completion-for-decorator-model-arg-2024-4-6-16-27-17.md deleted file mode 100644 index 6318a4f9848..00000000000 --- a/.chronus/changes/add-completion-for-decorator-model-arg-2024-4-6-16-27-17.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -changeKind: feature -packages: - - "@typespec/compiler" ---- - -Support completion for object values and model expression properties. - - Example - ```tsp - model User { - name: string; - age: int32; - address: string; - } - - const user: User = #{name: "Bob", ┆} - | [age] - | [address] - ``` - diff --git a/.chronus/changes/add_madeRequired-2024-4-9-21-13-8.md b/.chronus/changes/add_madeRequired-2024-4-9-21-13-8.md deleted file mode 100644 index 3202e2fa009..00000000000 --- a/.chronus/changes/add_madeRequired-2024-4-9-21-13-8.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: fix -packages: - - "@typespec/versioning" ---- - -Add `@madeRequired` decorator diff --git a/.chronus/changes/feat-fix-indentation-in-doc-comments-2024-4-20-14-31-22.md b/.chronus/changes/feat-fix-indentation-in-doc-comments-2024-4-20-14-31-22.md deleted file mode 100644 index f6d9cd81b09..00000000000 --- a/.chronus/changes/feat-fix-indentation-in-doc-comments-2024-4-20-14-31-22.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: internal -packages: - - "@typespec/http" - - "@typespec/protobuf" - - "@typespec/rest" - - "@typespec/versioning" - - "@typespec/xml" ---- - -Regen docs diff --git a/.chronus/changes/feat-fix-union-variant-decorators-2024-4-20-14-27-15.md b/.chronus/changes/feat-fix-union-variant-decorators-2024-4-20-14-27-15.md deleted file mode 100644 index a5d6e21a81c..00000000000 --- a/.chronus/changes/feat-fix-union-variant-decorators-2024-4-20-14-27-15.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: internal -packages: - - "@typespec/compiler" ---- -Doc diff --git a/.chronus/changes/feat-fix-union-variant-decorators-2024-4-20-14-48-3.md b/.chronus/changes/feat-fix-union-variant-decorators-2024-4-20-14-48-3.md deleted file mode 100644 index 836c4235ecd..00000000000 --- a/.chronus/changes/feat-fix-union-variant-decorators-2024-4-20-14-48-3.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: fix -packages: - - "@typespec/json-schema" ---- - -Fix decorators application for union variants diff --git a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-1.md b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-1.md deleted file mode 100644 index cc0ab5bb76b..00000000000 --- a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-1.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: deprecation -packages: - - "@typespec/compiler" ---- - -Using a tuple type as a value is deprecated. Tuple types in contexts where values are expected must be updated to be array values instead. A codefix is provided to automatically convert tuple types into array values. - -```tsp -model Test { - // Deprecated - values: string[] = ["a", "b", "c"]; - - // Correct - values: string[] = #["a", "b", "c"]; -``` diff --git a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-2.md b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-2.md deleted file mode 100644 index de3f507c16e..00000000000 --- a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3-2.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: deprecation -packages: - - "@typespec/compiler" ---- - -Using a model type as a value is deprecated. Model types in contexts where values are expected must be updated to be object values instead. A codefix is provided to automatically convert model types into object values. - -```tsp -model Test { - // Deprecated - user: {name: string} = {name: "System"}; - - // Correct - user: {name: string} = #{name: "System"}; -``` diff --git a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3.md b/.chronus/changes/feature-object-literals-2024-2-15-18-36-3.md deleted file mode 100644 index 46b8e211dda..00000000000 --- a/.chronus/changes/feature-object-literals-2024-2-15-18-36-3.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: feature -packages: - - "@typespec/compiler" ---- - -Add syntax for declaring values. [See docs](https://typespec.io/docs/language-basics/values). - -Object and array values -```tsp -@dummy(#{ - name: "John", - age: 48, - address: #{ city: "London" } - aliases: #["Bob", "Frank"] -}) -``` - -Scalar constructors - -```tsp -scalar utcDateTime { - init fromISO(value: string); -} - -model DateRange { - minDate: utcDateTime = utcDateTime.fromISO("2024-02-15T18:36:03Z"); -} -``` diff --git a/.chronus/changes/feature-object-literals-2024-2-18-22-23-26.md b/.chronus/changes/feature-object-literals-2024-2-18-22-23-26.md deleted file mode 100644 index 966b10a2b47..00000000000 --- a/.chronus/changes/feature-object-literals-2024-2-18-22-23-26.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: fix -packages: - - "@typespec/json-schema" - - "@typespec/protobuf" - - "@typespec/versioning" ---- - -Update to support new value types diff --git a/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md b/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md deleted file mode 100644 index 60dd5fe793d..00000000000 --- a/.chronus/changes/feature-object-literals-2024-3-16-10-38-3.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -changeKind: deprecation -packages: - - "@typespec/compiler" ---- - -Decorator API: Legacy marshalling logic - -With the introduction of values, the decorator marshalling behavior has changed in some cases. This behavior is opt-in by setting the `valueMarshalling` package flag to `"new"`, but will be the default behavior in future versions. It is strongly recommended to adopt this new behavior as soon as possible. - - - Example: - ```tsp - extern dec multipleOf(target: numeric | Reflection.ModelProperty, value: valueof numeric); - ``` - Will now emit a deprecated warning because `value` is of type `valueof string` which would marshall to `Numeric` under the new logic but as `number` previously. - - To opt-in you can add the following to your library js/ts files. - ```ts - export const $flags = definePackageFlags({ - decoratorArgMarshalling: "new", - }); - ``` diff --git a/.chronus/changes/feature-object-literals-2024-3-16-11-58-32.md b/.chronus/changes/feature-object-literals-2024-3-16-11-58-32.md deleted file mode 100644 index 3a6f6da2098..00000000000 --- a/.chronus/changes/feature-object-literals-2024-3-16-11-58-32.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -changeKind: feature -packages: - - "@typespec/openapi3" ---- - -Add support for new object and array values as default values (e.g. `decimals: decimal[] = #[123, 456.7];`) diff --git a/.chronus/changes/feature-object-literals-2024-3-16-12-0-15.md b/.chronus/changes/feature-object-literals-2024-3-16-12-0-15.md deleted file mode 100644 index 83844db17a6..00000000000 --- a/.chronus/changes/feature-object-literals-2024-3-16-12-0-15.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -changeKind: fix -packages: - - "@typespec/rest" ---- - -Update types to support new values in TypeSpec \ No newline at end of file diff --git a/.chronus/changes/feature-object-literals-2024-3-16-17-54-23.md b/.chronus/changes/feature-object-literals-2024-3-16-17-54-23.md deleted file mode 100644 index 9855fea9fef..00000000000 --- a/.chronus/changes/feature-object-literals-2024-3-16-17-54-23.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: feature -packages: - - "@typespec/html-program-viewer" ---- - -Add support for values diff --git a/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md b/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md deleted file mode 100644 index 25e98493276..00000000000 --- a/.chronus/changes/feature-object-literals-32024-2-15-18-36-3.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: fix -packages: - - "@typespec/http" ---- - -Update Flow Template to make use of the new array values - diff --git a/.chronus/changes/fix-alias-indeterminate-entity-2024-4-16-19-9-9.md b/.chronus/changes/fix-alias-indeterminate-entity-2024-4-16-19-9-9.md deleted file mode 100644 index ebd01a696e2..00000000000 --- a/.chronus/changes/fix-alias-indeterminate-entity-2024-4-16-19-9-9.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: internal -packages: - - "@typespec/compiler" ---- -Fixing change from this release diff --git a/.chronus/changes/fix-compiler-circular-refs-2024-4-9-14-57-38.md b/.chronus/changes/fix-compiler-circular-refs-2024-4-9-14-57-38.md deleted file mode 100644 index fadfbea1df7..00000000000 --- a/.chronus/changes/fix-compiler-circular-refs-2024-4-9-14-57-38.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: internal -packages: - - "@typespec/compiler" - - "@typespec/http" - - "@typespec/playground" ---- diff --git a/.chronus/changes/feat-fix-indentation-in-doc-comments-2024-4-20-14-54-41.md b/.chronus/changes/fix-formatting-object-array-literal-hug-2024-5-13-20-4-7.md similarity index 66% rename from .chronus/changes/feat-fix-indentation-in-doc-comments-2024-4-20-14-54-41.md rename to .chronus/changes/fix-formatting-object-array-literal-hug-2024-5-13-20-4-7.md index 28b7152d033..49283b977e3 100644 --- a/.chronus/changes/feat-fix-indentation-in-doc-comments-2024-4-20-14-54-41.md +++ b/.chronus/changes/fix-formatting-object-array-literal-hug-2024-5-13-20-4-7.md @@ -5,4 +5,4 @@ packages: - "@typespec/compiler" --- -Preserve leading whitespace in fenced blocks in doc comments +Fix formatting of object and array literal in decorator to hug parenthesis diff --git a/.chronus/changes/fix-missing-export-2024-4-10-8-11-11.md b/.chronus/changes/fix-missing-export-2024-4-10-8-11-11.md deleted file mode 100644 index f441bba5f08..00000000000 --- a/.chronus/changes/fix-missing-export-2024-4-10-8-11-11.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -changeKind: internal -packages: - - "@typespec/compiler" ---- - diff --git a/.chronus/changes/fix-numeric-not-handling-trailing-zeros-2024-4-16-2-43-25.md b/.chronus/changes/fix-numeric-not-handling-trailing-zeros-2024-4-16-2-43-25.md deleted file mode 100644 index a821497cbde..00000000000 --- a/.chronus/changes/fix-numeric-not-handling-trailing-zeros-2024-4-16-2-43-25.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: fix -packages: - - "@typespec/compiler" ---- - -Numeric not handling trailing zeros and causing freeze(e.g. `const a = 100.0`) diff --git a/.chronus/changes/fix-vitest-2024-4-17-21-35-16.md b/.chronus/changes/fix-vitest-2024-4-17-21-35-16.md deleted file mode 100644 index b762765d84a..00000000000 --- a/.chronus/changes/fix-vitest-2024-4-17-21-35-16.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: internal -packages: - - "@typespec/openapi3" ---- - -Fix vitest not reloading JS files diff --git a/.chronus/changes/highlight-suppress-2024-4-28-16-43-58.md b/.chronus/changes/highlight-suppress-2024-4-28-16-43-58.md deleted file mode 100644 index 6b38355bc08..00000000000 --- a/.chronus/changes/highlight-suppress-2024-4-28-16-43-58.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -changeKind: feature -packages: - - "@typespec/compiler" ---- - -Hide deprecated items from completion list \ No newline at end of file diff --git a/.chronus/changes/json-schema-fixes-2024-4-25-0-58-10.md b/.chronus/changes/json-schema-fixes-2024-4-25-0-58-10.md deleted file mode 100644 index 878d132894b..00000000000 --- a/.chronus/changes/json-schema-fixes-2024-4-25-0-58-10.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -changeKind: fix -packages: - - "@typespec/compiler" ---- - -Emitter framework: fix losing context when referencing circular types \ No newline at end of file diff --git a/.chronus/changes/json-schema-fixes-2024-4-25-0-58-28.md b/.chronus/changes/json-schema-fixes-2024-4-25-0-58-28.md deleted file mode 100644 index d9fcbef1623..00000000000 --- a/.chronus/changes/json-schema-fixes-2024-4-25-0-58-28.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -changeKind: internal -packages: - - "@typespec/json-schema" ---- - diff --git a/.chronus/changes/json-schema-reftype-fix-2024-4-23-16-41-33.md b/.chronus/changes/json-schema-reftype-fix-2024-4-23-16-41-33.md deleted file mode 100644 index caeaa4c2d70..00000000000 --- a/.chronus/changes/json-schema-reftype-fix-2024-4-23-16-41-33.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -changeKind: fix -packages: - - "@typespec/json-schema" ---- - -The emitted JSON Schema now doesn't make root schemas for TypeSpec types which do not have the @jsonSchema decorator or are contained in a namespace with that decorator. Instead, such schemas are put into the $defs of any root schema which references them, and are referenced using JSON Pointers. diff --git a/.chronus/changes/playground-storybook-2024-4-22-18-10-6.md b/.chronus/changes/playground-storybook-2024-4-22-18-10-6.md deleted file mode 100644 index f03da4dbccd..00000000000 --- a/.chronus/changes/playground-storybook-2024-4-22-18-10-6.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: internal -packages: - - "@typespec/playground" ---- - -Add storybook for playground library diff --git a/.chronus/changes/playground-vite-2024-4-20-21-21-23.md b/.chronus/changes/playground-vite-2024-4-20-21-21-23.md deleted file mode 100644 index 9db9dd493e3..00000000000 --- a/.chronus/changes/playground-vite-2024-4-20-21-21-23.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: internal -packages: - - "@typespec/playground" ---- - -Simplify to use vite for building playground library diff --git a/.chronus/changes/upgrade-deps-may-2024-2024-4-20-15-37-30.md b/.chronus/changes/upgrade-deps-may-2024-2024-4-20-15-37-30.md deleted file mode 100644 index 0d90d57b86f..00000000000 --- a/.chronus/changes/upgrade-deps-may-2024-2024-4-20-15-37-30.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: dependencies -packages: - - "@typespec/bundler" - - "@typespec/compiler" - - "@typespec/eslint-plugin" - - "@typespec/html-program-viewer" - - "@typespec/http" - - "@typespec/internal-build-utils" - - "@typespec/json-schema" - - "@typespec/library-linter" - - "@typespec/openapi" - - "@typespec/openapi3" - - "@typespec/playground" - - "@typespec/prettier-plugin-typespec" - - "@typespec/protobuf" - - "@typespec/rest" - - tmlanguage-generator - - typespec-vscode - - "@typespec/versioning" - - "@typespec/xml" ---- - -Update dependencies - May 2024 diff --git a/.chronus/changes/versioning-FixVersionBug1-2024-4-21-16-34-18.md b/.chronus/changes/versioning-FixVersionBug1-2024-4-21-16-34-18.md deleted file mode 100644 index 1b66d63ed6f..00000000000 --- a/.chronus/changes/versioning-FixVersionBug1-2024-4-21-16-34-18.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: fix -packages: - - "@typespec/versioning" ---- - -Using `@removed` on member types and `@added` on containing type could result in errors diff --git a/.chronus/changes/versioning-library-cleanup-2024-4-9-22-35-15.md b/.chronus/changes/versioning-library-cleanup-2024-4-9-22-35-15.md deleted file mode 100644 index bd60b190891..00000000000 --- a/.chronus/changes/versioning-library-cleanup-2024-4-9-22-35-15.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking -changeKind: internal -packages: - - "@typespec/versioning" ---- - -Organize versioning library diff --git a/.chronus/changes/vscode-output-cmd-2024-4-17-16-6-25.md b/.chronus/changes/vscode-output-cmd-2024-4-17-16-6-25.md deleted file mode 100644 index a5d7a26cec6..00000000000 --- a/.chronus/changes/vscode-output-cmd-2024-4-17-16-6-25.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -changeKind: feature -packages: - - typespec-vscode ---- - -Add 'TypeSpec: Show Output Channel' command in VSCode extension \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 44c5a17a2f7..b9955f2c324 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,13 +1,13 @@ +# Catch all +* @bterlson @markcowl @allenjzhang @timotheeguerin + ###################### # CSharp ###################### -/packages/http-client-csharp/ @m-nash -/packages/http-client-csharp-generator/ @m-nash +/packages/http-client-csharp/ @m-nash @JoshLove-msft ###################### # Emiter Shared ###################### /eng/emitters/ @m-nash -# Catch all -* @bterlson @markcowl @allenjzhang @timotheeguerin diff --git a/.gitignore b/.gitignore index c5cc09c2a7a..986a7f7c740 100644 --- a/.gitignore +++ b/.gitignore @@ -194,3 +194,4 @@ docs/**/js-api/ # csharp emitter !packages/http-client-csharp/package-lock.json packages/http-client-csharp/generator/artifacts/ +BenchmarkDotnet.Artifacts/ diff --git a/.vscode/launch.json b/.vscode/launch.json index a65d4eafe34..802349582d5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -108,6 +108,7 @@ "type": "extensionHost", "request": "launch", "args": ["--extensionDevelopmentPath=${workspaceFolder}/packages/typespec-vscode"], + "outFiles": ["${workspaceFolder}/packages/typespec-vscode/dist/**/*.cjs"], "env": { // Log elapsed time for each call to server. //"TYPESPEC_SERVER_LOG_TIMING": "true", diff --git a/.vscode/settings.json b/.vscode/settings.json index f8e14796463..8e894d52f42 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,7 @@ "**/node_modules/**": true, "packages/compiler/templates/__snapshots__/**": true, "packages/website/versioned_docs/**": true, + "packages/http-client-csharp/**/Generated/**": true, "packages/samples/scratch/**": false // Those files are in gitignore but we still want to search for them }, "files.exclude": { diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8473e71a21a..d0b3ffdda81 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Developper guide +# Developer guide This section goes over the setup of the repo for development. @@ -374,8 +374,8 @@ Misc labels ### Updating labels -Labels are configured in `eng/common/labels.yaml`. To update labels, edit this file and run `pnpm sync-labels`. -**If you create a new label in github UI without updating the `labels.yaml` file, it WILL be automatically removed** +Labels are configured in `eng/common/config/labels.ts`. To update labels, edit this file and run `pnpm sync-labels`. +**If you create a new label in github UI without updating the `labels.ts` file, it WILL be automatically removed** # TypeSpec Emitters diff --git a/cspell.yaml b/cspell.yaml index 2270b4c67db..0ffb1bb753a 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -29,6 +29,7 @@ words: - dbaeumer - debouncer - devdiv + - Diagnoser - dogfood - eastus - ecmarkup @@ -75,6 +76,7 @@ words: - onwarn - openapi - openapiv + - Perfolizer - picocolors - prismjs - proto diff --git a/docs/emitters/json-schema/reference/decorators.md b/docs/emitters/json-schema/reference/decorators.md index a61dd90562f..e8445e5ad45 100644 --- a/docs/emitters/json-schema/reference/decorators.md +++ b/docs/emitters/json-schema/reference/decorators.md @@ -258,6 +258,22 @@ Specify that the numeric type must be a multiple of some numeric value. | ----- | ----------------- | -------------------------------------------------- | | value | `valueof numeric` | The numeric type must be a multiple of this value. | +### `@oneOf` {#@TypeSpec.JsonSchema.oneOf} + +Specify that `oneOf` should be used instead of `anyOf` for that union. + +```typespec +@TypeSpec.JsonSchema.oneOf +``` + +#### Target + +`Union | ModelProperty` + +#### Parameters + +None + ### `@prefixItems` {#@TypeSpec.JsonSchema.prefixItems} Specify that the target array must begin with the provided types. diff --git a/docs/emitters/json-schema/reference/index.mdx b/docs/emitters/json-schema/reference/index.mdx index bbc10f06d42..ce0f343fb01 100644 --- a/docs/emitters/json-schema/reference/index.mdx +++ b/docs/emitters/json-schema/reference/index.mdx @@ -52,6 +52,7 @@ npm install --save-peer @typespec/json-schema - [`@minContains`](./decorators.md#@TypeSpec.JsonSchema.minContains) - [`@minProperties`](./decorators.md#@TypeSpec.JsonSchema.minProperties) - [`@multipleOf`](./decorators.md#@TypeSpec.JsonSchema.multipleOf) +- [`@oneOf`](./decorators.md#@TypeSpec.JsonSchema.oneOf) - [`@prefixItems`](./decorators.md#@TypeSpec.JsonSchema.prefixItems) - [`@uniqueItems`](./decorators.md#@TypeSpec.JsonSchema.uniqueItems) diff --git a/docs/getting-started/typespec-for-openapi-dev.md b/docs/getting-started/typespec-for-openapi-dev.md index 90d6e8a0249..a7c5b42d064 100644 --- a/docs/getting-started/typespec-for-openapi-dev.md +++ b/docs/getting-started/typespec-for-openapi-dev.md @@ -244,13 +244,13 @@ and can contain markdown formatting. ```typespec @doc(""" -Get status info for the service. -The status includes the current version of the service. -The status value may be one of: -- `ok`: the service is operating normally -- `degraded`: the service is operating in a degraded state -- `down`: the service is not operating -""") + Get status info for the service. + The status includes the current version of the service. + The status value may be one of: + - `ok`: the service is operating normally + - `degraded`: the service is operating in a degraded state + - `down`: the service is not operating + """) @tag("Status") @route("/status") @get diff --git a/docs/introduction/editor/vs.md b/docs/introduction/editor/vs.md index d5dd81272a4..c546c7933db 100644 --- a/docs/introduction/editor/vs.md +++ b/docs/introduction/editor/vs.md @@ -4,7 +4,7 @@ title: TypeSpec Visual Studio Extension ## Installation -Install the extension via the Visual Studio extension manager from the [TypeSpec for Visual Studio - Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=typespec.typespec-vs). +Install the extension via the Visual Studio extension manager from the [TypeSpec for Visual Studio - Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=typespec.typespecvs). ## Configure diff --git a/docs/language-basics/type-literals.md b/docs/language-basics/type-literals.md index 049c990d7d6..2de0abad55f 100644 --- a/docs/language-basics/type-literals.md +++ b/docs/language-basics/type-literals.md @@ -21,10 +21,10 @@ Multi-line string literals are denoted using three double quotes `"""`. ```typespec alias Str = """ -This is a multi line string - - opt 1 - - opt 2 -"""; + This is a multi line string + - opt 1 + - opt 2 + """; ``` - The opening `"""` must be followed by a new line. diff --git a/docs/libraries/http/reference/data-types.md b/docs/libraries/http/reference/data-types.md index 109a8f83730..dc8106701e9 100644 --- a/docs/libraries/http/reference/data-types.md +++ b/docs/libraries/http/reference/data-types.md @@ -205,6 +205,20 @@ model TypeSpec.Http.CreatedResponse | ---------- | ----- | ---------------- | | statusCode | `201` | The status code. | +### `File` {#TypeSpec.Http.File} + +```typespec +model TypeSpec.Http.File +``` + +#### Properties + +| Name | Type | Description | +| ------------ | -------- | ----------- | +| contentType? | `string` | | +| filename? | `string` | | +| contents | `bytes` | | + ### `ForbiddenResponse` {#TypeSpec.Http.ForbiddenResponse} Access is forbidden. @@ -234,6 +248,35 @@ model TypeSpec.Http.HeaderOptions | name? | `string` | Name of the header when sent over HTTP. | | format? | `"csv" \| "multi" \| "tsv" \| "ssv" \| "pipes" \| "simple" \| "form"` | Determines the format of the array if type array is used. | +### `HttpPart` {#TypeSpec.Http.HttpPart} + +```typespec +model TypeSpec.Http.HttpPart +``` + +#### Template Parameters + +| Name | Description | +| ------- | ----------- | +| Type | | +| Options | | + +#### Properties + +None + +### `HttpPartOptions` {#TypeSpec.Http.HttpPartOptions} + +```typespec +model TypeSpec.Http.HttpPartOptions +``` + +#### Properties + +| Name | Type | Description | +| ----- | -------- | ------------------------------------------- | +| name? | `string` | Name of the part when using the array form. | + ### `ImplicitFlow` {#TypeSpec.Http.ImplicitFlow} Implicit flow diff --git a/docs/libraries/http/reference/decorators.md b/docs/libraries/http/reference/decorators.md index a9dfefd0dca..26c688c55b2 100644 --- a/docs/libraries/http/reference/decorators.md +++ b/docs/libraries/http/reference/decorators.md @@ -225,6 +225,32 @@ Specify if inapplicable metadata should be included in the payload for the given | ----- | ----------------- | --------------------------------------------------------------- | | value | `valueof boolean` | If true, inapplicable metadata will be included in the payload. | +### `@multipartBody` {#@TypeSpec.Http.multipartBody} + +```typespec +@TypeSpec.Http.multipartBody +``` + +#### Target + +`ModelProperty` + +#### Parameters + +None + +#### Examples + +```tsp +op upload( + @header `content-type`: "multipart/form-data", + @multipartBody body: { + fullName: HttpPart; + headShots: HttpPart[]; + }, +): void; +``` + ### `@patch` {#@TypeSpec.Http.patch} Specify the HTTP verb for the target operation to be `PATCH`. @@ -462,8 +488,13 @@ None #### Examples ```typespec -op read(): {@statusCode: 200, @body pet: Pet} -op create(): {@statusCode: 201 | 202} +op read(): { + @statusCode _: 200; + @body pet: Pet; +}; +op create(): { + @statusCode _: 201 | 202; +}; ``` ### `@useAuth` {#@TypeSpec.Http.useAuth} diff --git a/docs/libraries/http/reference/index.mdx b/docs/libraries/http/reference/index.mdx index ec9a88f52e8..d85da81df69 100644 --- a/docs/libraries/http/reference/index.mdx +++ b/docs/libraries/http/reference/index.mdx @@ -43,6 +43,7 @@ npm install --save-peer @typespec/http - [`@head`](./decorators.md#@TypeSpec.Http.head) - [`@header`](./decorators.md#@TypeSpec.Http.header) - [`@includeInapplicableMetadataInPayload`](./decorators.md#@TypeSpec.Http.includeInapplicableMetadataInPayload) +- [`@multipartBody`](./decorators.md#@TypeSpec.Http.multipartBody) - [`@patch`](./decorators.md#@TypeSpec.Http.patch) - [`@path`](./decorators.md#@TypeSpec.Http.path) - [`@post`](./decorators.md#@TypeSpec.Http.post) @@ -66,8 +67,11 @@ npm install --save-peer @typespec/http - [`ClientCredentialsFlow`](./data-types.md#TypeSpec.Http.ClientCredentialsFlow) - [`ConflictResponse`](./data-types.md#TypeSpec.Http.ConflictResponse) - [`CreatedResponse`](./data-types.md#TypeSpec.Http.CreatedResponse) +- [`File`](./data-types.md#TypeSpec.Http.File) - [`ForbiddenResponse`](./data-types.md#TypeSpec.Http.ForbiddenResponse) - [`HeaderOptions`](./data-types.md#TypeSpec.Http.HeaderOptions) +- [`HttpPart`](./data-types.md#TypeSpec.Http.HttpPart) +- [`HttpPartOptions`](./data-types.md#TypeSpec.Http.HttpPartOptions) - [`ImplicitFlow`](./data-types.md#TypeSpec.Http.ImplicitFlow) - [`LocationHeader`](./data-types.md#TypeSpec.Http.LocationHeader) - [`MovedResponse`](./data-types.md#TypeSpec.Http.MovedResponse) diff --git a/docs/release-notes/release-2024-02-06.md b/docs/release-notes/release-2024-02-06.md index 6e1d98b877e..0ff2d186160 100644 --- a/docs/release-notes/release-2024-02-06.md +++ b/docs/release-notes/release-2024-02-06.md @@ -6,7 +6,7 @@ title: February 2024 ## Release of VSCode and Visual Studio extensions -- [VSCode Extension](https://marketplace.visualstudio.com/items?itemName=typespec.vscode-extension) +- [VSCode Extension](https://marketplace.visualstudio.com/items?itemName=typespec.typespec-vscode) - [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=typespec.typespecvs) ## New Features diff --git a/docs/release-notes/release-2024-06-10.md b/docs/release-notes/release-2024-06-10.md new file mode 100644 index 00000000000..5e37300aee7 --- /dev/null +++ b/docs/release-notes/release-2024-06-10.md @@ -0,0 +1,259 @@ +--- +title: 0.57 - June 2024 +--- + +:::warning +This release contains deprecations +::: + +## Values In TypeSpec + +Decorators often expect some sort of configuration to be passed as an argument. In version `0.51.0` we introduced the `valueof` keyword to constrain a decorator or template parameter to be a value, however this was limited to string, numeric and boolean values. +When dealing with more complex decorators or free form data, one had to use a model type to represent the data. Unfortunately, this approach was difficult for decorator authors to handle reliably and required somewhat sophisticated validation to avoid some common pitfalls that couldn't be prevented through type system constraints. + +In this release we introduce a new concept to TypeSpec: values. This includes new syntax for declaring object and array values, scalar constructors, and `const` declarations. + +### Object values + +Object values can be declared with the `#{}` syntax. + +```tsp +const user = #{ name: "Bob", age: 48, address: #{ city: "London" } }; +``` + +### Array values + +Array values can be declared with the `#[]` syntax. + +```tsp +const users = #["Bob", "Frank"]; +``` + +### `const` + +Const declares a variable that holds a value, and can optionally specify a type. + +```tsp +const name = "Bob"; +const name: string = "Bob"; +``` + +### Scalar constructors + +Scalars can define custom values with scalar value constructors. Scalars which don't extend a base type like `string`, `number` or `boolean` can define a constructor. + +```tsp +scalar ipV4 { + init fromInt(value: int32); +} +const ip: ipV4 = ipV4.fromInt(3232235776); +``` + +[See values doc for more info](https://typespec.io/docs/language-basics/values) + +## Deprecations + +### @typespec/compiler + +- [#3022](https://github.com/microsoft/typespec/pull/3022) Using a tuple type as a value is deprecated. Tuple types in contexts where values are expected must be updated to be array values instead. A codefix is provided to automatically convert tuple types into array values. + +```tsp +model Test { + // Deprecated + values: string[] = ["a", "b", "c"]; + + // Correct + values: string[] = #["a", "b", "c"]; +} +``` + +- [#3022](https://github.com/microsoft/typespec/pull/3022) Using a model type as a value is deprecated. Model types in contexts where values are expected must be updated to be object values instead. A codefix is provided to automatically convert model types into object values. + +```tsp +model Test { + // Deprecated + user: { + name: string; + } = { + name: "System"; + }; + + // Correct + user: { + name: string; + } = #{ name: "System" }; +} +``` + +- [#3022](https://github.com/microsoft/typespec/pull/3022) Decorator API: Legacy marshalling logic + +With the introduction of values, the decorator marshalling behavior has changed in some cases. This behavior is opt-in by setting the `valueMarshalling` package flag to `"new"`, but will be the default behavior in future versions. It is strongly recommended to adopt this new behavior as soon as possible. + +Example: + +```tsp +extern dec multipleOf(target: numeric | Reflection.ModelProperty, value: valueof numeric); +``` + +Will now emit a deprecated warning because `value` is of type `valueof string` which would marshall to `Numeric` under the new logic but as `number` previously. + +To opt-in you can add the following to your library js/ts files. + +```ts +export const $flags = definePackageFlags({ + decoratorArgMarshalling: "new", +}); +``` + +## Features + +### @typespec/compiler + +- [#3280](https://github.com/microsoft/typespec/pull/3280) Support completion for Model with extended properties + + Example + + ```tsp + model Device { + name: string; + description: string; + } + + model Phone extends Device { + ┆ + } | [name] + | [description] + ``` + +- [#3280](https://github.com/microsoft/typespec/pull/3280) Support completion for object values and model expression properties. + + Example + + ```tsp + model User { + name: string; + age: int32; + address: string; + } + + const user: User = #{name: "Bob", ┆} + | [age] + | [address] + ``` + +- [#3375](https://github.com/microsoft/typespec/pull/3375) Allow `@` to be escaped in doc comment with `\` +- [#3022](https://github.com/microsoft/typespec/pull/3022) Add syntax for declaring values. [See docs](https://typespec.io/docs/language-basics/values). + +Object and array values + +```tsp +@dummy(#{ + name: "John", + age: 48, + address: #{ city: "London" } + aliases: #["Bob", "Frank"] +}) +``` + +Scalar constructors + +```tsp +scalar utcDateTime { + init fromISO(value: string); +} + +model DateRange { + minDate: utcDateTime = utcDateTime.fromISO("2024-02-15T18:36:03Z"); +} +``` + +- [#3527](https://github.com/microsoft/typespec/pull/3527) Add support for `@prop` doc comment tag to describe model properties +- [#3460](https://github.com/microsoft/typespec/pull/3460) Hide deprecated items from completion list +- [#3462](https://github.com/microsoft/typespec/pull/3462) Linter `all` rulesets is automatically created if not explicitly provided +- [#3533](https://github.com/microsoft/typespec/pull/3533) More logs and traces are added for diagnostic and troubleshooting in TypeSpec language server +- [#3422](https://github.com/microsoft/typespec/pull/3422) Formatter: Indent or dedent multiline strings to the current indentation + +### @typespec/http + +- [#3342](https://github.com/microsoft/typespec/pull/3342) Add new multipart handling. Using `@multipartBody` with `HttpPart`. See [multipart docs](https://typespec.io/docs/next/libraries/http/multipart) for more information. + + ```tsp + op upload( + @header contentType: "multipart/mixed", + @multipartBody body: { + name: HttpPart; + avatar: HttpPart[]; + }, + ): void; + ``` + +- [#3462](https://github.com/microsoft/typespec/pull/3462) Use new compiler automatic `all` ruleset instead of explicitly provided one + +### @typespec/openapi3 + +- [#3022](https://github.com/microsoft/typespec/pull/3022) Add support for new object and array values as default values (e.g. `decimals: decimal[] = #[123, 456.7];`) + +### @typespec/html-program-viewer + +- [#3022](https://github.com/microsoft/typespec/pull/3022) Add support for values + +### @typespec/json-schema + +- [#3557](https://github.com/microsoft/typespec/pull/3557) Add support for @oneOf decorator. + +### typespec-vs + +- [#3461](https://github.com/microsoft/typespec/pull/3461) Support Arm64 + +### typespec-vscode + +- [#3533](https://github.com/microsoft/typespec/pull/3533) Enhance logging and trace + +1. Support "Developer: Set Log Level..." command to filter logs in TypeSpec output channel +2. Add "typespecLanguageServer.trace.server" config for whether and how to send the traces from TypeSpec language server to client. (It still depends on client to decide whether to show these traces based on the configured Log Level.) +3. More logs and traces are added for diagnostic and troubleshooting + +- [#3385](https://github.com/microsoft/typespec/pull/3385) Add 'TypeSpec: Show Output Channel' command in VSCode extension + +## Bug Fixes + +### @typespec/compiler + +- [#3399](https://github.com/microsoft/typespec/pull/3399) Preserve leading whitespace in fenced blocks in doc comments +- [#3522](https://github.com/microsoft/typespec/pull/3522) Fix EINVAL error when running `tsp code install` +- [#3371](https://github.com/microsoft/typespec/pull/3371) Numeric not handling trailing zeros and causing freeze(e.g. `const a = 100.0`) +- [#3451](https://github.com/microsoft/typespec/pull/3451) Emitter framework: fix losing context when referencing circular types +- [#3517](https://github.com/microsoft/typespec/pull/3517) Fix application of `@param` doc tag on operation create with `op is` to override upstream doc +- [#3488](https://github.com/microsoft/typespec/pull/3488) Add `PickProperties` type to dynamically select a subset of a model + +### @typespec/http + +- [#3022](https://github.com/microsoft/typespec/pull/3022) Update Flow Template to make use of the new array values + +### @typespec/versioning + +- [#3292](https://github.com/microsoft/typespec/pull/3292) Add `@madeRequired` decorator +- [#3022](https://github.com/microsoft/typespec/pull/3022) Update to support new value types +- [#3409](https://github.com/microsoft/typespec/pull/3409) Using `@removed` on member types and `@added` on containing type could result in errors +- [#3255](https://github.com/microsoft/typespec/pull/3255) If a property were marked with @added on a later version, the logic that said it was originally added on the first version was erroneously removed, resulting in incorrect projections. + +### @typespec/rest + +- [#3022](https://github.com/microsoft/typespec/pull/3022) Update types to support new values in TypeSpec + +### @typespec/openapi3 + +- [#3342](https://github.com/microsoft/typespec/pull/3342) Add support for new multipart constructs in http library +- [#3574](https://github.com/microsoft/typespec/pull/3574) Emit diagnostic when an invalid type is used as a property instead of crashing. + +### @typespec/protobuf + +- [#3022](https://github.com/microsoft/typespec/pull/3022) Update to support new value types +- [#3561](https://github.com/microsoft/typespec/pull/3561) Corrected cross-package reference behavior in some buggy cases. +- + +### @typespec/json-schema + +- [#3398](https://github.com/microsoft/typespec/pull/3398) Fix decorators application for union variants +- [#3022](https://github.com/microsoft/typespec/pull/3022) Update to support new value types +- [#3430](https://github.com/microsoft/typespec/pull/3430) The emitted JSON Schema now doesn't make root schemas for TypeSpec types which do not have the @jsonSchema decorator or are contained in a namespace with that decorator. Instead, such schemas are put into the $defs of any root schema which references them, and are referenced using JSON Pointers. diff --git a/docs/standard-library/built-in-data-types.md b/docs/standard-library/built-in-data-types.md index 719158cd911..d058b9dbe9d 100644 --- a/docs/standard-library/built-in-data-types.md +++ b/docs/standard-library/built-in-data-types.md @@ -96,6 +96,23 @@ model OptionalProperties | Source | An object whose spread properties are all optional. | +#### Properties +None + +### `PickProperties` {#PickProperties} + +Represents a collection of properties with only the specified keys included. +```typespec +model PickProperties +``` + +#### Template Parameters +| Name | Description | +|------|-------------| +| Source | An object whose properties are spread. | +| Keys | The property keys to include. | + + #### Properties None diff --git a/docs/standard-library/built-in-decorators.md b/docs/standard-library/built-in-decorators.md index b705b4b5023..86336bf0c31 100644 --- a/docs/standard-library/built-in-decorators.md +++ b/docs/standard-library/built-in-decorators.md @@ -933,6 +933,24 @@ Returns the model with the given properties omitted. +### `@withPickedProperties` {#@withPickedProperties} + +Returns the model with only the given properties included. +```typespec +@withPickedProperties(pick: string | Union) +``` + +#### Target + +`Model` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| pick | `string \| Union` | List of properties to include | + + + ### `@withUpdateableProperties` {#@withUpdateableProperties} Returns the model with non-updateable properties removed. diff --git a/grammars/typespec.json b/grammars/typespec.json index 3929a1558e5..4cd712c7571 100644 --- a/grammars/typespec.json +++ b/grammars/typespec.json @@ -228,7 +228,7 @@ }, "doc-comment-param": { "name": "comment.block.tsp", - "match": "(?x)((@)(?:param|template))\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)\\b", + "match": "(?x)((@)(?:param|template|prop))\\s+(\\b[_$[:alpha:]][_$[:alnum:]]*\\b|`(?:[^`\\\\]|\\\\.)*`)\\b", "captures": { "1": { "name": "keyword.tag.tspdoc" diff --git a/package.json b/package.json index b563077a97d..043e84aab07 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,9 @@ "purge": "rimraf --glob \"packages/*/node_modules/\"", "regen-docs": "pnpm -r --parallel --aggregate-output --reporter=append-only run regen-docs", "regen-samples": "pnpm -r run regen-samples", - "test:ci": "pnpm -r --aggregate-output --reporter=append-only --sequential test:ci", + "test": "vitest run", + "test:ci": "vitest run --coverage --reporter=junit --reporter=default", "test:e2e": "pnpm -r run test:e2e", - "test": "pnpm -r --aggregate-output --reporter=append-only run test", "update-latest-docs": "pnpm -r run update-latest-docs", "watch": "tsc --build ./tsconfig.ws.json --watch", "sync-labels": "tsx ./eng/common/scripts/labels/sync-labels.ts --config ./eng/common/config/labels.ts" @@ -46,6 +46,7 @@ "@types/node": "~18.11.19", "@typescript-eslint/parser": "^7.9.0", "@typescript-eslint/utils": "^7.9.0", + "@vitest/coverage-v8": "^1.6.0", "c8": "^9.1.0", "cspell": "^8.8.1", "eslint": "^8.57.0", diff --git a/packages/bundle-uploader/package.json b/packages/bundle-uploader/package.json index 6452d4926a1..3afa5fc1de1 100644 --- a/packages/bundle-uploader/package.json +++ b/packages/bundle-uploader/package.json @@ -37,7 +37,7 @@ "!dist/test/**" ], "dependencies": { - "@azure/identity": "~4.2.0", + "@azure/identity": "~4.2.1", "@azure/storage-blob": "~12.18.0", "@pnpm/find-workspace-packages": "^6.0.9", "@typespec/bundler": "workspace:~", diff --git a/packages/bundler/CHANGELOG.md b/packages/bundler/CHANGELOG.md index 224a3e54990..d98cc3df621 100644 --- a/packages/bundler/CHANGELOG.md +++ b/packages/bundler/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log - @typespec/bundler +## 0.1.4 + +### Bump dependencies + +- [#3401](https://github.com/microsoft/typespec/pull/3401) Update dependencies - May 2024 + + ## 0.1.3 ### Bump dependencies diff --git a/packages/bundler/package.json b/packages/bundler/package.json index 585feaa9322..2882199a117 100644 --- a/packages/bundler/package.json +++ b/packages/bundler/package.json @@ -1,6 +1,6 @@ { "name": "@typespec/bundler", - "version": "0.1.3", + "version": "0.1.4", "author": "Microsoft Corporation", "description": "Package to bundle a TypeSpec library.", "homepage": "https://typespec.io", diff --git a/packages/compiler/CHANGELOG.md b/packages/compiler/CHANGELOG.md index 4c6cd01c3a2..ae3b417513e 100644 --- a/packages/compiler/CHANGELOG.md +++ b/packages/compiler/CHANGELOG.md @@ -1,5 +1,142 @@ # Change Log - @typespec/compiler +## 0.57.0 + +### Bug Fixes + +- [#3399](https://github.com/microsoft/typespec/pull/3399) Preserve leading whitespace in fenced blocks in doc comments +- [#3566](https://github.com/microsoft/typespec/pull/3566) [API] Do not run decorators on cloned type if the original type wasn't finished +- [#3522](https://github.com/microsoft/typespec/pull/3522) Fix EINVAL error when running `tsp code install` +- [#3371](https://github.com/microsoft/typespec/pull/3371) Numeric not handling trailing zeros and causing freeze(e.g. `const a = 100.0`) +- [#3451](https://github.com/microsoft/typespec/pull/3451) Emitter framework: fix losing context when referencing circular types +- [#3517](https://github.com/microsoft/typespec/pull/3517) Fix application of `@param` doc tag on operation create with `op is` to override upstream doc +- [#3488](https://github.com/microsoft/typespec/pull/3488) Add `PickProperties` type to dynamically select a subset of a model + +### Bump dependencies + +- [#3401](https://github.com/microsoft/typespec/pull/3401) Update dependencies - May 2024 + +### Features + +- [#3280](https://github.com/microsoft/typespec/pull/3280) Support completion for Model with extended properties + + Example + ```tsp + model Device { + name: string; + description: string; + } + + model Phone extends Device { + ┆ + } | [name] + | [description] + ``` +- [#3280](https://github.com/microsoft/typespec/pull/3280) Support completion for object values and model expression properties. + + Example + ```tsp + model User { + name: string; + age: int32; + address: string; + } + + const user: User = #{name: "Bob", ┆} + | [age] + | [address] + ``` +- [#3375](https://github.com/microsoft/typespec/pull/3375) Allow `@` to be escaped in doc comment with `\` +- [#3022](https://github.com/microsoft/typespec/pull/3022) Add syntax for declaring values. [See docs](https://typespec.io/docs/language-basics/values). + +Object and array values +```tsp +@dummy(#{ + name: "John", + age: 48, + address: #{ city: "London" } + aliases: #["Bob", "Frank"] +}) +``` + +Scalar constructors + +```tsp +scalar utcDateTime { + init fromISO(value: string); +} + +model DateRange { + minDate: utcDateTime = utcDateTime.fromISO("2024-02-15T18:36:03Z"); +} +``` +- [#3527](https://github.com/microsoft/typespec/pull/3527) Add support for `@prop` doc comment tag to describe model properties +- [#3422](https://github.com/microsoft/typespec/pull/3422) Formatter: Indent or dedent multiline strings to the current indentation +- [#3460](https://github.com/microsoft/typespec/pull/3460) Hide deprecated items from completion list +- [#3443](https://github.com/microsoft/typespec/pull/3443) Support completion for keyword 'extends' and 'is' + + Example + ```tsp + model Dog ┆ {} + | [extends] + | [is] + + scalar Addresss ┆ + | [extends] + + op jump ┆ + | [is] + + interface ResourceA ┆ {} + | [extends] + + model Cat {} + | [extends] + ``` +- [#3462](https://github.com/microsoft/typespec/pull/3462) Linter `all` rulesets is automatically created if not explicitly provided +- [#3533](https://github.com/microsoft/typespec/pull/3533) More logs and traces are added for diagnostic and troubleshooting in TypeSpec language server + +### Deprecations + +- [#3022](https://github.com/microsoft/typespec/pull/3022) Using a tuple type as a value is deprecated. Tuple types in contexts where values are expected must be updated to be array values instead. A codefix is provided to automatically convert tuple types into array values. + +```tsp +model Test { + // Deprecated + values: string[] = ["a", "b", "c"]; + + // Correct + values: string[] = #["a", "b", "c"]; +``` +- [#3022](https://github.com/microsoft/typespec/pull/3022) Using a model type as a value is deprecated. Model types in contexts where values are expected must be updated to be object values instead. A codefix is provided to automatically convert model types into object values. + +```tsp +model Test { + // Deprecated + user: {name: string} = {name: "System"}; + + // Correct + user: {name: string} = #{name: "System"}; +``` +- [#3022](https://github.com/microsoft/typespec/pull/3022) Decorator API: Legacy marshalling logic + +With the introduction of values, the decorator marshalling behavior has changed in some cases. This behavior is opt-in by setting the `valueMarshalling` package flag to `"new"`, but will be the default behavior in future versions. It is strongly recommended to adopt this new behavior as soon as possible. + + + Example: + ```tsp + extern dec multipleOf(target: numeric | Reflection.ModelProperty, value: valueof numeric); + ``` + Will now emit a deprecated warning because `value` is of type `valueof string` which would marshall to `Numeric` under the new logic but as `number` previously. + + To opt-in you can add the following to your library js/ts files. + ```ts + export const $flags = definePackageFlags({ + decoratorArgMarshalling: "new", + }); + ``` + + ## 0.56.0 ### Bug Fixes diff --git a/packages/compiler/generated-defs/TypeSpec.ts b/packages/compiler/generated-defs/TypeSpec.ts index 489bc02cad5..5b2724f7ea3 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts @@ -76,6 +76,17 @@ export type WithoutOmittedPropertiesDecorator = ( omit: Type ) => void; +/** + * Returns the model with only the given properties included. + * + * @param pick List of properties to include + */ +export type WithPickedPropertiesDecorator = ( + context: DecoratorContext, + target: Model, + pick: Type +) => void; + /** * Returns the model with any default values removed. */ diff --git a/packages/compiler/generated-defs/TypeSpec.ts-test.ts b/packages/compiler/generated-defs/TypeSpec.ts-test.ts index 84b84078316..b923f63be00 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts-test.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts-test.ts @@ -35,6 +35,7 @@ import { $visibility, $withDefaultKeyVisibility, $withOptionalProperties, + $withPickedProperties, $withUpdateableProperties, $withVisibility, $withoutDefaultValues, @@ -76,6 +77,7 @@ import type { VisibilityDecorator, WithDefaultKeyVisibilityDecorator, WithOptionalPropertiesDecorator, + WithPickedPropertiesDecorator, WithUpdateablePropertiesDecorator, WithVisibilityDecorator, WithoutDefaultValuesDecorator, @@ -88,6 +90,7 @@ type Decorators = { $withOptionalProperties: WithOptionalPropertiesDecorator; $withUpdateableProperties: WithUpdateablePropertiesDecorator; $withoutOmittedProperties: WithoutOmittedPropertiesDecorator; + $withPickedProperties: WithPickedPropertiesDecorator; $withoutDefaultValues: WithoutDefaultValuesDecorator; $withDefaultKeyVisibility: WithDefaultKeyVisibilityDecorator; $summary: SummaryDecorator; @@ -131,6 +134,7 @@ const _: Decorators = { $withOptionalProperties, $withUpdateableProperties, $withoutOmittedProperties, + $withPickedProperties, $withoutDefaultValues, $withDefaultKeyVisibility, $summary, diff --git a/packages/compiler/lib/std/decorators.tsp b/packages/compiler/lib/std/decorators.tsp index 2259b95bdc2..79ece74013e 100644 --- a/packages/compiler/lib/std/decorators.tsp +++ b/packages/compiler/lib/std/decorators.tsp @@ -604,6 +604,12 @@ extern dec withoutDefaultValues(target: Model); */ extern dec withoutOmittedProperties(target: Model, omit: string | Union); +/** + * Returns the model with only the given properties included. + * @param pick List of properties to include + */ +extern dec withPickedProperties(target: Model, pick: string | Union); + //--------------------------------------------------------------------------- // Debugging //--------------------------------------------------------------------------- diff --git a/packages/compiler/lib/std/types.tsp b/packages/compiler/lib/std/types.tsp index 7f02346fe3d..12fa180add0 100644 --- a/packages/compiler/lib/std/types.tsp +++ b/packages/compiler/lib/std/types.tsp @@ -53,6 +53,18 @@ model OmitProperties { ...Source; } +/** + * Represents a collection of properties with only the specified keys included. + * + * @template Source An object whose properties are spread. + * @template Keys The property keys to include. + */ +@doc("The template for picking properties.") +@withPickedProperties(Keys) +model PickProperties { + ...Source; +} + /** * Represents a collection of properties with default values omitted. * diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 4435723f603..06d7bf203a6 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@typespec/compiler", - "version": "0.56.0", + "version": "0.57.0", "description": "TypeSpec Compiler Preview", "author": "Microsoft Corporation", "license": "MIT", diff --git a/packages/compiler/src/core/charcode.ts b/packages/compiler/src/core/charcode.ts index c493a080783..3a06f2e32a0 100644 --- a/packages/compiler/src/core/charcode.ts +++ b/packages/compiler/src/core/charcode.ts @@ -247,17 +247,20 @@ export function isNonAsciiIdentifierCharacter(codePoint: number) { return lookupInNonAsciiMap(codePoint, nonAsciiIdentifierMap); } -export function codePointBefore(text: string, pos: number): number | undefined { - if (pos <= 0 || pos >= text.length) { - return undefined; +export function codePointBefore( + text: string, + pos: number +): { char: number | undefined; size: number } { + if (pos <= 0 || pos > text.length) { + return { char: undefined, size: 0 }; } const ch = text.charCodeAt(pos - 1); if (!isLowSurrogate(ch) || !isHighSurrogate(text.charCodeAt(pos - 2))) { - return ch; + return { char: ch, size: 1 }; } - return text.codePointAt(pos - 2); + return { char: text.codePointAt(pos - 2), size: 2 }; } function lookupInNonAsciiMap(codePoint: number, map: readonly number[]) { diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 246d4e3e846..37ca0715e16 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -47,6 +47,7 @@ import { } from "./parser.js"; import type { Program, ProjectedProgram } from "./program.js"; import { createProjectionMembers } from "./projection-members.js"; +import { Realm } from "./realm.js"; import { getFullyQualifiedSymbolName, getParentTemplateNode, @@ -248,7 +249,7 @@ export interface Checker { createAndFinishType( typeDef: T ): T & TypePrototype; - finishType(typeDef: T): T; + finishType(typeDef: T, realm?: Realm): T; createFunctionType(fn: (...args: Type[]) => Type): FunctionType; createLiteralType(value: string, node?: StringLiteralNode): StringLiteral; createLiteralType(value: number, node?: NumericLiteralNode): NumericLiteral; @@ -349,6 +350,7 @@ export function createChecker(program: Program): Checker { const stdTypes: Partial = {}; const symbolLinks = new Map(); const mergedSymbols = new Map(); + const docFromCommentForSym = new Map(); const augmentDecoratorsForSym = new Map(); const augmentedSymbolTables = new Map(); const referenceSymCache = new WeakMap< @@ -2494,6 +2496,21 @@ export function createChecker(program: Program): Checker { const name = node.id.sv; let decorators: DecoratorApplication[] = []; + const parameterModelSym = getOrCreateAugmentedSymbolTable(symbol!.metatypeMembers!).get( + "parameters" + ); + + if (parameterModelSym?.members) { + const members = getOrCreateAugmentedSymbolTable(parameterModelSym.members); + const paramDocs = extractParamDocs(node); + for (const [name, memberSym] of members) { + const doc = paramDocs.get(name); + if (doc) { + docFromCommentForSym.set(memberSym, doc); + } + } + } + // Is this a definition or reference? let parameters: Model, returnType: Type, sourceOperation: Operation | undefined; if (node.signature.kind === SyntaxKind.OperationSignatureReference) { @@ -2501,9 +2518,6 @@ export function createChecker(program: Program): Checker { const baseOperation = checkOperationIs(node, node.signature.baseOperation, mapper); if (baseOperation) { sourceOperation = baseOperation; - const parameterModelSym = getOrCreateAugmentedSymbolTable(symbol!.metatypeMembers!).get( - "parameters" - )!; // Reference the same return type and create the parameters type const clone = initializeClone(baseOperation.parameters, { properties: createRekeyableMap(), @@ -2512,7 +2526,7 @@ export function createChecker(program: Program): Checker { clone.properties = createRekeyableMap( Array.from(baseOperation.parameters.properties.entries()).map(([key, prop]) => [ key, - cloneTypeForSymbol(getMemberSymbol(parameterModelSym, prop.name)!, prop, { + cloneTypeForSymbol(getMemberSymbol(parameterModelSym!, prop.name)!, prop, { model: clone, sourceProperty: prop, }), @@ -3832,6 +3846,18 @@ export function createChecker(program: Program): Checker { derivedModels: [], }); linkType(links, type, mapper); + + if (node.symbol.members) { + const members = getOrCreateAugmentedSymbolTable(node.symbol.members); + const propDocs = extractPropDocs(node); + for (const [name, memberSym] of members) { + const doc = propDocs.get(name); + if (doc) { + docFromCommentForSym.set(memberSym, doc); + } + } + } + const isBase = checkModelIs(node, node.is, mapper); if (isBase) { @@ -3847,7 +3873,8 @@ export function createChecker(program: Program): Checker { if (isBase) { for (const prop of isBase.properties.values()) { - const newProp = cloneType(prop, { + const memberSym = getMemberSymbol(node.symbol, prop.name)!; + const newProp = cloneTypeForSymbol(memberSym, prop, { sourceProperty: prop, model: type, }); @@ -5113,7 +5140,8 @@ export function createChecker(program: Program): Checker { prop: ModelPropertyNode, mapper: TypeMapper | undefined ): ModelProperty { - const symId = getSymbolId(getSymbolForMember(prop)!); + const sym = getSymbolForMember(prop)!; + const symId = getSymbolId(sym); const links = getSymbolLinksForMember(prop); if (links && links.declaredType && mapper === undefined) { @@ -5160,14 +5188,9 @@ export function createChecker(program: Program): Checker { linkMapper(type, mapper); if (!parentTemplate || shouldCreateTypeForTemplate(parentTemplate, mapper)) { - if ( - prop.parent?.parent?.kind === SyntaxKind.OperationSignatureDeclaration && - prop.parent.parent.parent?.kind === SyntaxKind.OperationStatement - ) { - const doc = extractParamDoc(prop.parent.parent.parent, type.name); - if (doc) { - type.decorators.unshift(createDocFromCommentDecorator("self", doc)); - } + const docComment = docFromCommentForSym.get(sym); + if (docComment) { + type.decorators.unshift(createDocFromCommentDecorator("self", docComment)); } finishType(type); } @@ -5574,6 +5597,7 @@ export function createChecker(program: Program): Checker { if (returnTypesDocs.errors) { decorators.unshift(createDocFromCommentDecorator("errors", returnTypesDocs.errors)); } + } else if (targetType.kind === "ModelProperty") { } return decorators; } @@ -5598,7 +5622,6 @@ export function createChecker(program: Program): Checker { decorators, derivedScalars: [], }); - checkScalarConstructors(type, node, type.constructors, mapper); linkType(links, type, mapper); if (node.extends) { @@ -5608,6 +5631,7 @@ export function createChecker(program: Program): Checker { type.baseScalar.derivedScalars.push(type); } } + checkScalarConstructors(type, node, type.constructors, mapper); decorators.push(...checkDecorators(type, node, mapper)); if (mapper === undefined) { @@ -5672,6 +5696,19 @@ export function createChecker(program: Program): Checker { constructors: Map, mapper: TypeMapper | undefined ) { + if (parentScalar.baseScalar) { + for (const member of parentScalar.baseScalar.constructors.values()) { + const newConstructor: ScalarConstructor = cloneTypeForSymbol( + getMemberSymbol(node.symbol, member.name)!, + { + ...member, + scalar: parentScalar, + } + ); + linkIndirectMember(node, newConstructor, mapper); + constructors.set(member.name, newConstructor); + } + } for (const member of node.members) { const constructor = checkScalarConstructor(member, mapper, parentScalar); if (constructors.has(constructor.name as string)) { @@ -6489,7 +6526,10 @@ export function createChecker(program: Program): Checker { * recursively by the caller. */ function cloneType(type: T, additionalProps: Partial = {}): T { - const clone = finishType(initializeClone(type, additionalProps)); + let clone = initializeClone(type, additionalProps); + if (type.isFinished) { + clone = finishType(clone); + } const projection = projectionsByType.get(type); if (projection) { projectionsByType.set(clone, projection); @@ -6513,6 +6553,10 @@ export function createChecker(program: Program): Checker { ): T { let clone = initializeClone(type, additionalProps); if ("decorators" in clone) { + const docComment = docFromCommentForSym.get(sym); + if (docComment) { + clone.decorators.push(createDocFromCommentDecorator("self", docComment)); + } for (const dec of checkAugmentDecorators(sym, clone, undefined)) { clone.decorators.push(dec); } @@ -8425,18 +8469,34 @@ function extractReturnsDocs(type: Type): { return result; } -function extractParamDoc(node: OperationStatementNode, paramName: string): string | undefined { +function extractParamDocs(node: OperationStatementNode): Map { if (node.docs === undefined) { - return undefined; + return new Map(); } + const paramDocs = new Map(); for (const doc of node.docs) { for (const tag of doc.tags) { - if (tag.kind === SyntaxKind.DocParamTag && tag.paramName.sv === paramName) { - return getDocContent(tag.content); + if (tag.kind === SyntaxKind.DocParamTag) { + paramDocs.set(tag.paramName.sv, getDocContent(tag.content)); } } } - return undefined; + return paramDocs; +} + +function extractPropDocs(node: ModelStatementNode): Map { + if (node.docs === undefined) { + return new Map(); + } + const propDocs = new Map(); + for (const doc of node.docs) { + for (const tag of doc.tags) { + if (tag.kind === SyntaxKind.DocPropTag) { + propDocs.set(tag.propName.sv, getDocContent(tag.content)); + } + } + } + return propDocs; } function getDocContent(content: readonly DocContent[]) { diff --git a/packages/compiler/src/core/cli/utils.ts b/packages/compiler/src/core/cli/utils.ts index d1ae1666ad4..7d4ada83106 100644 --- a/packages/compiler/src/core/cli/utils.ts +++ b/packages/compiler/src/core/cli/utils.ts @@ -86,6 +86,7 @@ export function run( const finalOptions: SpawnSyncOptionsWithStringEncoding = { encoding: "utf-8", stdio: "inherit", + shell: process.platform === "win32", ...(options ?? {}), }; diff --git a/packages/compiler/src/core/index.ts b/packages/compiler/src/core/index.ts index c022d1fe183..8b96675bba5 100644 --- a/packages/compiler/src/core/index.ts +++ b/packages/compiler/src/core/index.ts @@ -40,7 +40,9 @@ export { setCadlNamespace, setTypeSpecNamespace, } from "./library.js"; +export { resolveLinterDefinition } from "./linter.js"; export * from "./module-resolver.js"; +export * from "./mutator.js"; export { NodeHost } from "./node-host.js"; export { Numeric, isNumeric } from "./numeric.js"; export * from "./options.js"; @@ -49,8 +51,10 @@ export * from "./parser.js"; export * from "./path-utils.js"; export * from "./program.js"; export { isProjectedProgram } from "./projected-program.js"; +export * from "./realm.js"; export * from "./scanner.js"; export * from "./semantic-walker.js"; export { createSourceFile, getSourceFileKindFromExt } from "./source-file.js"; +export * from "./type-factory.js"; export * from "./type-utils.js"; export * from "./types.js"; diff --git a/packages/compiler/src/core/linter.ts b/packages/compiler/src/core/linter.ts index cbb6a7f8cc1..93ebaa045cb 100644 --- a/packages/compiler/src/core/linter.ts +++ b/packages/compiler/src/core/linter.ts @@ -8,6 +8,7 @@ import { DiagnosticMessages, LibraryInstance, LinterDefinition, + LinterResolvedDefinition, LinterRule, LinterRuleContext, LinterRuleDiagnosticReport, @@ -22,6 +23,34 @@ export interface Linter { lint(): readonly Diagnostic[]; } +/** + * Resolve the linter definition + */ +export function resolveLinterDefinition( + libName: string, + linter: LinterDefinition +): LinterResolvedDefinition { + const rules: LinterRule[] = linter.rules.map((rule) => { + return { ...rule, id: `${libName}/${rule.name}` }; + }); + if (linter.rules.length === 0 || (linter.ruleSets && "all" in linter.ruleSets)) { + return { + rules, + ruleSets: linter.ruleSets ?? {}, + }; + } else { + return { + rules, + ruleSets: { + all: { + enable: Object.fromEntries(rules.map((x) => [x.id, true])) as any, + }, + ...linter.ruleSets, + }, + }; + } +} + export function createLinter( program: Program, loadLibrary: (name: string) => Promise @@ -37,11 +66,6 @@ export function createLinter( lint, }; - function getLinterDefinition(library: LibraryInstance): LinterDefinition | undefined { - // eslint-disable-next-line deprecation/deprecation - return library?.linter ?? library?.definition?.linter; - } - async function extendRuleSet(ruleSet: LinterRuleSet): Promise { tracer.trace("extend-rule-set.start", JSON.stringify(ruleSet, null, 2)); const diagnostics = createDiagnosticCollector(); @@ -50,7 +74,7 @@ export function createLinter( const ref = diagnostics.pipe(parseRuleReference(extendingRuleSetName)); if (ref) { const library = await resolveLibrary(ref.libraryName); - const libLinterDefinition = library && getLinterDefinition(library); + const libLinterDefinition = library?.linter; const extendingRuleSet = libLinterDefinition?.ruleSets?.[ref.name]; if (extendingRuleSet) { await extendRuleSet(extendingRuleSet); @@ -146,19 +170,17 @@ export function createLinter( tracer.trace("register-library", name); const library = await loadLibrary(name); - const linter = library && getLinterDefinition(library); + const linter = library?.linter; if (linter?.rules) { - for (const ruleDef of linter.rules) { - const ruleId = `${name}/${ruleDef.name}`; + for (const rule of linter.rules) { tracer.trace( "register-library.rule", - `Registering rule "${ruleId}" for library "${name}".` + `Registering rule "${rule.id}" for library "${name}".` ); - const rule: LinterRule = { ...ruleDef, id: ruleId }; - if (ruleMap.has(ruleId)) { - compilerAssert(false, `Unexpected duplicate linter rule: "${ruleId}"`); + if (ruleMap.has(rule.id)) { + compilerAssert(false, `Unexpected duplicate linter rule: "${rule.id}"`); } else { - ruleMap.set(ruleId, rule); + ruleMap.set(rule.id, rule); } } } diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 442a6f9833b..ad699221729 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -252,6 +252,7 @@ const diagnostics = { default: "Invalid identifier.", tag: "Invalid tag name. Use backticks around code if this was not meant to be a tag.", param: "Invalid parameter name.", + prop: "Invalid property name.", templateParam: "Invalid template parameter name.", }, }, diff --git a/packages/compiler/src/core/mutator.ts b/packages/compiler/src/core/mutator.ts new file mode 100644 index 00000000000..33b9640a224 --- /dev/null +++ b/packages/compiler/src/core/mutator.ts @@ -0,0 +1,386 @@ +import { CustomKeyMap } from "../emitter-framework/custom-key-map.js"; +import { $doc, isArrayModelType, isVisible } from "../index.js"; +import { Program } from "./program.js"; +import { Realm } from "./realm.js"; +import { + Decorator, + DecoratorFunction, + Enum, + EnumMember, + FunctionParameter, + FunctionType, + Interface, + IntrinsicType, + Model, + ModelProperty, + Namespace, + ObjectType, + Operation, + Projection, + Scalar, + ScalarConstructor, + StringTemplate, + StringTemplateSpan, + SyntaxKind, + TemplateParameter, + Tuple, + Type, + Union, + UnionVariant, +} from "./types.js"; + +export type MutatorRecord = + | { + filter?: MutatorFilterFn; + mutate: MutatorFn; + } + | { + filter?: MutatorFilterFn; + replace: MutatorReplaceFn; + } + | MutatorFn; + +export interface MutatorFn { + (sourceType: T, clone: T, program: Program, realm: Realm): void; +} + +export interface MutatorFilterFn { + (sourceType: T, program: Program, realm: Realm): boolean | MutatorFlow; +} + +export interface MutatorReplaceFn { + (sourceType: T, clone: T, program: Program, realm: Realm): Type; +} + +export interface Mutator { + name: string; + Model?: MutatorRecord; + ModelProperty?: MutatorRecord; + Scalar?: MutatorRecord; + Enum?: MutatorRecord; + EnumMember?: MutatorRecord; + Union?: MutatorRecord; + UnionVariant?: MutatorRecord; + Tuple?: MutatorRecord; + Operation?: MutatorRecord; + Interface?: MutatorRecord; + String?: MutatorRecord; + Number?: MutatorRecord; + Boolean?: MutatorRecord; + ScalarConstructor?: MutatorRecord; + StringTemplate?: MutatorRecord; + StringTemplateSpan?: MutatorRecord; +} + +export interface VisibilityOptions { + visibility: string; +} + +export enum MutatorFlow { + MutateAndRecurse = 0, + DontMutate = 1 << 0, + DontRecurse = 1 << 1, +} + +export function createVisibilityMutator(visibility: string): Mutator { + return { + name: visibility + " Visibility", + Model: { + filter(m, program, realm) { + if (isArrayModelType(program, m)) { + return MutatorFlow.DontMutate; + } + return true; + }, + mutate(m, clone, program, realm) { + if (clone.name) { + clone.name = m.name + visibility.charAt(0).toUpperCase() + visibility.slice(1); + } + + for (const prop of m.properties.values()) { + if (!isVisible(program, prop, [visibility])) { + clone.properties.delete(prop.name); + realm.remove(prop); + } + } + return; + }, + }, + }; +} + +const JSONMergePatch: Mutator = { + name: "JSON Merge Patch", + Model: { + filter(m, program, realm) { + // hissssss bad hissssss + if (m.node!.parent!.kind === SyntaxKind.OperationSignatureDeclaration) { + return MutatorFlow.DontMutate; + } + + return isArrayModelType(program, m) ? MutatorFlow.DontRecurse | MutatorFlow.DontMutate : true; + }, + mutate(sourceType, clone, program, realm) { + if (clone.name) { + clone.name = clone.name + "MergePatch"; + } + + for (const prop of clone.properties.values()) { + const clonedProp = realm.typeFactory.initializeClone(prop); + if (clonedProp.optional) { + if (clonedProp.type.kind === "Scalar") { + // remove everything but doc and apply it to the declaration + // TODO: THIS IS A HACK + const docDecorator = clonedProp.decorators.filter((d) => d.decorator === $doc); + const otherDecorators: [DecoratorFunction, ...any][] = clonedProp.decorators + .filter((d) => d.decorator !== $doc) + .map((d) => { + return [d.decorator, ...d.args.map((v) => v.jsValue)]; + }); + + clonedProp.decorators = docDecorator; + + const ginnedScalar = realm.typeFactory.scalar( + ...otherDecorators, + clone.name + prop.name[0].toUpperCase() + prop.name.slice(1), + { + extends: clonedProp.type, + } + ); + + clonedProp.type = realm.typeFactory.union([ginnedScalar, program.typeFactory.null]); + } else { + // otherwise pray it works, I guess + clonedProp.type = realm.typeFactory.union([clonedProp.type, program.typeFactory.null]); + } + } + clonedProp.optional = true; + clone.properties.set(prop.name, clonedProp); + realm.typeFactory.finishType(clonedProp); + } + }, + }, +}; + +export const Mutators = { + Visibility: { + create: createVisibilityMutator("create"), + read: createVisibilityMutator("read"), + update: createVisibilityMutator("update"), + delete: createVisibilityMutator("delete"), + query: createVisibilityMutator("query"), + }, + JSONMergePatch, +}; + +export type MutatableType = Exclude< + Type, + | TemplateParameter + | Namespace + | IntrinsicType + | FunctionType + | Decorator + | FunctionParameter + | ObjectType + | Projection +>; +const typeId = CustomKeyMap.objectKeyer(); +const mutatorId = CustomKeyMap.objectKeyer(); +const seen = new CustomKeyMap<[MutatableType, Set | Mutator[]], Type>( + ([type, mutators]) => { + const key = `${typeId.getKey(type)}-${[...mutators.values()] + .map((v) => mutatorId.getKey(v)) + .join("-")}`; + return key; + } +); +export function mutateSubgraph( + program: Program, + mutators: Mutator[], + type: T +): { realm: Realm | null; type: MutatableType } { + const realm = new Realm(program, "realm for mutation"); + const interstitials: (() => void)[] = []; + + const mutated = mutateSubgraphWorker(type, new Set(mutators)); + + if (mutated === type) { + return { realm: null, type }; + } else { + return { realm, type: mutated }; + } + + function mutateSubgraphWorker( + type: T, + activeMutators: Set + ): MutatableType { + let existing = seen.get([type, activeMutators]); + if (existing) { + cloneInterstitials(); + return existing as T; + } + + let clone: MutatableType | null = null; + const mutatorsWithOptions: { + mutator: Mutator; + mutationFn: MutatorFn | null; + replaceFn: MutatorReplaceFn | null; + }[] = []; + + // step 1: see what mutators to run + const newMutators = new Set(activeMutators.values()); + for (const mutator of activeMutators) { + const record = mutator[type.kind] as MutatorRecord | undefined; + if (!record) { + continue; + } + + let mutationFn: MutatorFn | null = null; + let replaceFn: MutatorReplaceFn | null = null; + + let mutate = false; + let recurse = false; + + if (typeof record === "function") { + mutationFn = record; + mutate = true; + recurse = true; + } else { + mutationFn = "mutate" in record ? record.mutate : null; + replaceFn = "replace" in record ? record.replace : null; + + if (record.filter) { + const filterResult = record.filter(type, program, realm); + if (filterResult === true) { + mutate = true; + recurse = true; + } else if (filterResult === false) { + mutate = false; + recurse = true; + } else { + mutate = (filterResult & MutatorFlow.DontMutate) === 0; + recurse = (filterResult & MutatorFlow.DontRecurse) === 0; + } + } else { + mutate = true; + recurse = true; + } + } + + if (!recurse) { + newMutators.delete(mutator); + } + + if (mutate) { + mutatorsWithOptions.push({ mutator, mutationFn, replaceFn }); + } + } + + const mutatorsToApply = mutatorsWithOptions.map((v) => v.mutator); + + // if we have no mutators to apply, let's bail out. + if (mutatorsWithOptions.length === 0) { + if (newMutators.size > 0) { + // we might need to clone this type later if something in our subgraph needs mutated. + interstitials.push(initializeClone); + visitSubgraph(); + interstitials.pop(); + return clone ?? type; + } else { + // we don't need to clone this type, so let's just return it. + return type; + } + } + + // step 2: see if we need to mutate based on the set of mutators we're actually going to run + existing = seen.get([type, mutatorsToApply]); + if (existing) { + cloneInterstitials(); + return existing as T; + } + + // step 3: run the mutators + cloneInterstitials(); + initializeClone(); + + for (const { mutationFn, replaceFn } of mutatorsWithOptions) { + // todo: handle replace earlier in the mutation chain + const result: MutatableType = (mutationFn! ?? replaceFn!)( + type, + clone! as any, + program, + realm + ) as any; + + if (replaceFn && result !== undefined) { + clone = result; + seen.set([type, activeMutators], clone); + seen.set([type, mutatorsToApply], clone); + } + } + + if (newMutators.size > 0) { + visitSubgraph(); + } + + realm.typeFactory.finishType(clone!); + + return clone!; + + function initializeClone() { + clone = realm.typeFactory.initializeClone(type); + seen.set([type, activeMutators], clone); + seen.set([type, mutatorsToApply], clone); + } + + function cloneInterstitials() { + for (const interstitial of interstitials) { + interstitial(); + } + + interstitials.length = 0; + } + + function visitSubgraph() { + const root = clone ?? type; + switch (root.kind) { + case "Model": + for (const prop of root.properties.values()) { + const newProp = mutateSubgraphWorker(prop, newMutators); + + if (clone) { + (clone as any).properties.set(prop.name, newProp); + } + } + if (root.indexer) { + const res = mutateSubgraphWorker(root.indexer.value as any, newMutators); + if (clone) { + (clone as any).indexer.value = res; + } + } + break; + case "ModelProperty": + const newType = mutateSubgraphWorker(root.type as MutatableType, newMutators); + if (clone) { + (clone as any).type = newType; + } + + break; + case "Operation": + const newParams = mutateSubgraphWorker(root.parameters, newMutators); + if (clone) { + (clone as any).parameters = newParams; + } + + break; + case "Scalar": + const newBaseScalar = root.baseScalar + ? mutateSubgraphWorker(root.baseScalar, newMutators) + : undefined; + if (clone) { + (clone as any).baseScalar = newBaseScalar; + } + } + } + } +} diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index af9c92ffede..29176357087 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -1,5 +1,5 @@ import { isArray, mutate } from "../utils/misc.js"; -import { trim } from "./charcode.js"; +import { codePointBefore, isIdentifierContinue, trim } from "./charcode.js"; import { compilerAssert } from "./diagnostics.js"; import { CompilerDiagnostics, createDiagnostic } from "./messages.js"; import { @@ -12,6 +12,9 @@ import { isPunctuation, isStatementKeyword, isTrivia, + skipContinuousIdentifier, + skipTrivia, + skipTriviaBackward, } from "./scanner.js"; import { AliasStatementNode, @@ -34,9 +37,11 @@ import { DocErrorsTagNode, DocNode, DocParamTagNode, + DocPropTagNode, DocReturnsTagNode, DocTag, DocTemplateTagNode, + DocTextNode, DocUnknownTagNode, EmptyStatementNode, EnumMemberNode, @@ -134,6 +139,17 @@ type ParseListItem = K extends UnannotatedListKind ? () => T : (pos: number, decorators: DecoratorExpressionNode[]) => T; +type ListDetail = { + items: T[]; + /** + * The range of the list items as below as an example + * model Foo { a: string; b: string; } + * + * remark: if the start/end token (i.e. { } ) not found, pos/end will be -1 + */ + range: TextRange; +}; + type OpenToken = | Token.OpenBrace | Token.OpenParen @@ -700,9 +716,10 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa ): InterfaceStatementNode { parseExpected(Token.InterfaceKeyword); const id = parseIdentifier(); - const templateParameters = parseTemplateParameterList(); + const { items: templateParameters, range: templateParametersRange } = + parseTemplateParameterList(); - let extendList: TypeReferenceNode[] = []; + let extendList: ListDetail = createEmptyList(); if (token() === Token.ExtendsKeyword) { nextToken(); extendList = parseList(ListKind.Heritage, parseReferenceExpression); @@ -711,25 +728,28 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa nextToken(); } - const operations = parseList(ListKind.InterfaceMembers, (pos, decorators) => - parseOperationStatement(pos, decorators, true) + const { items: operations, range: bodyRange } = parseList( + ListKind.InterfaceMembers, + (pos, decorators) => parseOperationStatement(pos, decorators, true) ); return { kind: SyntaxKind.InterfaceStatement, id, templateParameters, + templateParametersRange, operations, - extends: extendList, + bodyRange, + extends: extendList.items, decorators, ...finishNode(pos), }; } - function parseTemplateParameterList(): TemplateParameterDeclarationNode[] { - const list = parseOptionalList(ListKind.TemplateParameters, parseTemplateParameter); + function parseTemplateParameterList(): ListDetail { + const detail = parseOptionalList(ListKind.TemplateParameters, parseTemplateParameter); let setDefault = false; - for (const item of list) { + for (const item of detail.items) { if (!item.default && setDefault) { error({ code: "default-required", target: item }); continue; @@ -740,7 +760,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa } } - return list; + return detail; } function parseUnionStatement( @@ -749,14 +769,16 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa ): UnionStatementNode { parseExpected(Token.UnionKeyword); const id = parseIdentifier(); - const templateParameters = parseTemplateParameterList(); + const { items: templateParameters, range: templateParametersRange } = + parseTemplateParameterList(); - const options = parseList(ListKind.UnionVariants, parseUnionVariant); + const { items: options } = parseList(ListKind.UnionVariants, parseUnionVariant); return { kind: SyntaxKind.UnionStatement, id, templateParameters, + templateParametersRange, decorators, options, ...finishNode(pos), @@ -833,7 +855,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa } const id = parseIdentifier(); - const templateParameters = parseTemplateParameterList(); + const { items: templateParameters, range: templateParametersRange } = + parseTemplateParameterList(); // Make sure the next token is one that is expected const token = expectTokenIsOneOf(Token.OpenParen, Token.IsKeyword); @@ -872,6 +895,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa kind: SyntaxKind.OperationStatement, id, templateParameters, + templateParametersRange, signature, decorators, ...finishNode(pos), @@ -880,10 +904,14 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseOperationParameters(): ModelExpressionNode { const pos = tokenPos(); - const properties = parseList(ListKind.OperationParameters, parseModelPropertyOrSpread); + const { items: properties, range: bodyRange } = parseList( + ListKind.OperationParameters, + parseModelPropertyOrSpread + ); const parameters: ModelExpressionNode = { kind: SyntaxKind.ModelExpression, properties, + bodyRange, ...finishNode(pos), }; return parameters; @@ -895,23 +923,26 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa ): ModelStatementNode { parseExpected(Token.ModelKeyword); const id = parseIdentifier(); - const templateParameters = parseTemplateParameterList(); + const { items: templateParameters, range: templateParametersRange } = + parseTemplateParameterList(); expectTokenIsOneOf(Token.OpenBrace, Token.Equals, Token.ExtendsKeyword, Token.IsKeyword); const optionalExtends = parseOptionalModelExtends(); const optionalIs = optionalExtends ? undefined : parseOptionalModelIs(); - let properties: (ModelPropertyNode | ModelSpreadPropertyNode)[] = []; + let propDetail: ListDetail = createEmptyList< + ModelPropertyNode | ModelSpreadPropertyNode + >(); if (optionalIs) { const tok = expectTokenIsOneOf(Token.Semicolon, Token.OpenBrace); if (tok === Token.Semicolon) { nextToken(); } else { - properties = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread); + propDetail = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread); } } else { - properties = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread); + propDetail = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread); } return { @@ -920,8 +951,10 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa extends: optionalExtends, is: optionalIs, templateParameters, + templateParametersRange, decorators, - properties, + properties: propDetail.items, + bodyRange: propDetail.range, ...finishNode(pos), }; } @@ -1090,17 +1123,20 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa ): ScalarStatementNode { parseExpected(Token.ScalarKeyword); const id = parseIdentifier(); - const templateParameters = parseTemplateParameterList(); + const { items: templateParameters, range: templateParametersRange } = + parseTemplateParameterList(); const optionalExtends = parseOptionalScalarExtends(); - const members = parseScalarMembers(); + const { items: members, range: bodyRange } = parseScalarMembers(); return { kind: SyntaxKind.ScalarStatement, id, templateParameters, + templateParametersRange, extends: optionalExtends, members, + bodyRange, decorators, ...finishNode(pos), }; @@ -1113,10 +1149,10 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa return undefined; } - function parseScalarMembers(): readonly ScalarConstructorNode[] { + function parseScalarMembers(): ListDetail { if (token() === Token.Semicolon) { nextToken(); - return []; + return createEmptyList(); } else { return parseList(ListKind.ScalarMembers, parseScalarMember); } @@ -1130,7 +1166,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa parseExpected(Token.InitKeyword); const id = parseIdentifier(); - const parameters = parseFunctionParameters(); + const { items: parameters } = parseFunctionParameters(); return { kind: SyntaxKind.ScalarConstructor, id, @@ -1145,7 +1181,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa ): EnumStatementNode { parseExpected(Token.EnumKeyword); const id = parseIdentifier(); - const members = parseList(ListKind.EnumMembers, parseEnumMemberOrSpread); + const { items: members } = parseList(ListKind.EnumMembers, parseEnumMemberOrSpread); return { kind: SyntaxKind.EnumStatement, id, @@ -1212,7 +1248,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseAliasStatement(pos: number): AliasStatementNode { parseExpected(Token.AliasKeyword); const id = parseIdentifier(); - const templateParameters = parseTemplateParameterList(); + const { items: templateParameters, range: templateParametersRange } = + parseTemplateParameterList(); parseExpected(Token.Equals); const value = parseExpression(); parseExpected(Token.Semicolon); @@ -1220,6 +1257,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa kind: SyntaxKind.AliasStatement, id, templateParameters, + templateParametersRange, value, ...finishNode(pos), }; @@ -1386,10 +1424,11 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const pos = tokenPos(); const target = parseIdentifierOrMemberExpression(message); if (token() === Token.OpenParen) { + const { items: args } = parseList(ListKind.FunctionArguments, parseExpression); return { kind: SyntaxKind.CallExpression, target, - arguments: parseList(ListKind.FunctionArguments, parseExpression), + arguments: args, ...finishNode(pos), }; } @@ -1401,7 +1440,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa target: IdentifierNode | MemberExpressionNode, pos: number ): TypeReferenceNode { - const args = parseOptionalList(ListKind.TemplateArguments, parseTemplateArgument); + const { items: args } = parseOptionalList(ListKind.TemplateArguments, parseTemplateArgument); return { kind: SyntaxKind.TypeReference, @@ -1461,29 +1500,31 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa // `@` applied to `model Foo`, and not as `@model` // applied to invalid statement `Foo`. const target = parseIdentifierOrMemberExpression(undefined, false); - const args = parseOptionalList(ListKind.DecoratorArguments, parseExpression); + const { items: args } = parseOptionalList(ListKind.DecoratorArguments, parseExpression); if (args.length === 0) { error({ code: "augment-decorator-target" }); + const emptyList = createEmptyList(); return { kind: SyntaxKind.AugmentDecoratorStatement, target, targetType: { kind: SyntaxKind.TypeReference, target: createMissingIdentifier(), - arguments: [], + arguments: emptyList.items, ...finishNode(pos), }, - arguments: [], + arguments: args, ...finishNode(pos), }; } let [targetEntity, ...decoratorArgs] = args; if (targetEntity.kind !== SyntaxKind.TypeReference) { error({ code: "augment-decorator-target", target: targetEntity }); + const emptyList = createEmptyList(); targetEntity = { kind: SyntaxKind.TypeReference, target: createMissingIdentifier(), - arguments: [], + arguments: emptyList.items, ...finishNode(pos), }; } @@ -1521,7 +1562,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa // `@` applied to `model Foo`, and not as `@model` // applied to invalid statement `Foo`. const target = parseIdentifierOrMemberExpression(undefined, false); - const args = parseOptionalList(ListKind.DecoratorArguments, parseExpression); + const { items: args } = parseOptionalList(ListKind.DecoratorArguments, parseExpression); return { kind: SyntaxKind.DecoratorExpression, arguments: args, @@ -1721,7 +1762,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseTupleExpression(): TupleExpressionNode { const pos = tokenPos(); - const values = parseList(ListKind.Tuple, parseExpression); + const { items: values } = parseList(ListKind.Tuple, parseExpression); return { kind: SyntaxKind.TupleExpression, values, @@ -1731,30 +1772,35 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseModelExpression(): ModelExpressionNode { const pos = tokenPos(); - const properties = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread); + const { items: properties, range: bodyRange } = parseList( + ListKind.ModelProperties, + parseModelPropertyOrSpread + ); return { kind: SyntaxKind.ModelExpression, properties, + bodyRange, ...finishNode(pos), }; } function parseObjectLiteral(): ObjectLiteralNode { const pos = tokenPos(); - const properties = parseList( + const { items: properties, range: bodyRange } = parseList( ListKind.ObjectLiteralProperties, parseObjectLiteralPropertyOrSpread ); return { kind: SyntaxKind.ObjectLiteral, properties, + bodyRange, ...finishNode(pos), }; } function parseArrayLiteral(): ArrayLiteralNode { const pos = tokenPos(); - const values = parseList(ListKind.ArrayLiteral, parseExpression); + const { items: values } = parseList(ListKind.ArrayLiteral, parseExpression); return { kind: SyntaxKind.ArrayLiteral, values, @@ -1977,7 +2023,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const modifierFlags = modifiersToFlags(modifiers); parseExpected(Token.DecKeyword); const id = parseIdentifier(); - let [target, ...parameters] = parseFunctionParameters(); + const allParamListDetail = parseFunctionParameters(); + let [target, ...parameters] = allParamListDetail.items; if (target === undefined) { error({ code: "decorator-decl-target", target: { pos, end: previousTokenEnd } }); target = { @@ -2011,7 +2058,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const modifierFlags = modifiersToFlags(modifiers); parseExpected(Token.FnKeyword); const id = parseIdentifier(); - const parameters = parseFunctionParameters(); + const { items: parameters } = parseFunctionParameters(); let returnType; if (parseOptional(Token.Colon)) { returnType = parseExpression(); @@ -2028,14 +2075,14 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa }; } - function parseFunctionParameters(): FunctionParameterNode[] { + function parseFunctionParameters(): ListDetail { const parameters = parseList( ListKind.FunctionParameters, parseFunctionParameter ); let foundOptional = false; - for (const [index, item] of parameters.entries()) { + for (const [index, item] of parameters.items.entries()) { if (!item.optional && foundOptional) { error({ code: "required-parameter-first", target: item }); continue; @@ -2048,7 +2095,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa if (item.rest && item.optional) { error({ code: "rest-parameter-required", target: item }); } - if (item.rest && index !== parameters.length - 1) { + if (item.rest && index !== parameters.items.length - 1) { error({ code: "rest-parameter-last", target: item }); } } @@ -2148,7 +2195,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa let parameters: ProjectionParameterDeclarationNode[]; if (token() === Token.OpenParen) { - parameters = parseList(ListKind.ProjectionParameter, parseProjectionParameter); + parameters = parseList(ListKind.ProjectionParameter, parseProjectionParameter).items; } else { parameters = []; } @@ -2393,7 +2440,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa kind: SyntaxKind.ProjectionCallExpression, callKind: "method", target: expr, - arguments: parseList(ListKind.CallArguments, parseProjectionExpression), + arguments: parseList(ListKind.CallArguments, parseProjectionExpression).items, ...finishNode(pos), }; } else { @@ -2484,7 +2531,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseProjectionLambdaOrParenthesizedExpression(): ProjectionExpression { const pos = tokenPos(); - const exprs = parseList(ListKind.ProjectionExpression, parseProjectionExpression); + const exprs = parseList(ListKind.ProjectionExpression, parseProjectionExpression).items; if (token() === Token.EqualsGreaterThan) { // unpack the exprs (which should be just identifiers) into a param list const params: ProjectionLambdaParameterDeclarationNode[] = []; @@ -2542,7 +2589,10 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseProjectionModelExpression(): ProjectionModelExpressionNode { const pos = tokenPos(); - const properties = parseList(ListKind.ModelProperties, parseProjectionModelPropertyOrSpread); + const { items: properties } = parseList( + ListKind.ModelProperties, + parseProjectionModelPropertyOrSpread + ); return { kind: SyntaxKind.ProjectionModelExpression, properties, @@ -2636,7 +2686,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseProjectionTupleExpression(): ProjectionTupleExpressionNode { const pos = tokenPos(); - const values = parseList(ListKind.Tuple, parseProjectionExpression); + const { items: values } = parseList(ListKind.Tuple, parseProjectionExpression); return { kind: SyntaxKind.ProjectionTupleExpression, values, @@ -2850,6 +2900,12 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa } nextToken(); break; + case Token.DocText: + parts.push(source.substring(start, tokenPos())); + parts.push(tokenValue()); + nextToken(); + start = tokenPos(); + break; default: nextToken(); break; @@ -2885,6 +2941,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa return parseDocParamLikeTag(pos, tagName, SyntaxKind.DocParamTag, "param"); case "template": return parseDocParamLikeTag(pos, tagName, SyntaxKind.DocTemplateTag, "templateParam"); + case "prop": + return parseDocPropTag(pos, tagName); case "return": case "returns": return parseDocSimpleTag(pos, tagName, SyntaxKind.DocReturnsTag); @@ -2905,9 +2963,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa kind: ParamLikeTag["kind"], messageId: keyof CompilerDiagnostics["doc-invalid-identifier"] ): ParamLikeTag { - const name = parseDocIdentifier(messageId); - parseOptionalHyphenDocParamLikeTag(); - const content = parseDocContent(); + const { name, content } = parseDocParamLikeTagInternal(messageId); return { kind, @@ -2918,6 +2974,27 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa }; } + function parseDocPropTag(pos: number, tagName: IdentifierNode): DocPropTagNode { + const { name, content } = parseDocParamLikeTagInternal("prop"); + + return { + kind: SyntaxKind.DocPropTag, + tagName, + propName: name, + content, + ...finishNode(pos), + }; + } + + function parseDocParamLikeTagInternal( + messageId: keyof CompilerDiagnostics["doc-invalid-identifier"] + ): { name: IdentifierNode; content: DocTextNode[] } { + const name = parseDocIdentifier(messageId); + parseOptionalHyphenDocParamLikeTag(); + const content = parseDocContent(); + return { name, content }; + } + /** * Handles the optional hyphen in param-like documentation comment tags. * @@ -3062,11 +3139,12 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function createMissingTypeReference(): TypeReferenceNode { const pos = tokenPos(); + const { items: args } = createEmptyList(); return { kind: SyntaxKind.TypeReference, target: createMissingIdentifier(), - arguments: [], + arguments: args, ...finishNode(pos), }; } @@ -3082,6 +3160,13 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa return obj as any; } + function createEmptyList(range: TextRange = { pos: -1, end: -1 }): ListDetail { + return { + items: [], + range, + }; + } + /** * Parse a delimited list of elements, including the surrounding open and * close punctuation @@ -3100,16 +3185,20 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseList( kind: K, parseItem: ParseListItem - ): T[] { + ): ListDetail { + const r: ListDetail = createEmptyList(); if (kind.open !== Token.None) { - parseExpected(kind.open); + const t = tokenPos(); + if (parseExpected(kind.open)) { + mutate(r.range).pos = t; + } } if (kind.allowEmpty && parseOptional(kind.close)) { - return []; + mutate(r.range).end = previousTokenEnd; + return r; } - const items: T[] = []; while (true) { const startingPos = tokenPos(); const { pos, docs, directives, decorators } = parseAnnotations({ @@ -3125,7 +3214,9 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa // of file. Note, however, that we must parse a missing element if // there were directives or decorators as we cannot drop those from // the tree. - parseExpected(kind.close); + if (parseExpected(kind.close)) { + mutate(r.range).end = previousTokenEnd; + } break; } @@ -3138,13 +3229,14 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa mutate(item).directives = directives; } - items.push(item); + r.items.push(item); const delimiter = token(); const delimiterPos = tokenPos(); if (parseOptionalDelimiter(kind)) { // Delimiter found: check if it's trailing. if (parseOptional(kind.close)) { + mutate(r.range).end = previousTokenEnd; if (!kind.trailingDelimiterIsValid) { error({ code: "trailing-token", @@ -3166,6 +3258,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa // there's no delimiter after an item. break; } else if (parseOptional(kind.close)) { + mutate(r.range).end = previousTokenEnd; // If a list *is* surrounded by punctuation, then the list ends when we // reach the close token. break; @@ -3175,7 +3268,9 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa // assumption that the closing delimiter is missing. This check is // duplicated from above to preempt the parseExpected(delimeter) // below. - parseExpected(kind.close); + if (parseExpected(kind.close)) { + mutate(r.range).end = previousTokenEnd; + } break; } else { // Error recovery: if a list kind *is* surrounded by punctuation and we @@ -3194,16 +3289,17 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa // // Simple repro: `model M { ]` would loop forever without this check. // - parseExpected(kind.close); + if (parseExpected(kind.close)) { + mutate(r.range).end = previousTokenEnd; + } nextToken(); // remove the item that was entirely inserted by error recovery. - items.pop(); + r.items.pop(); break; } } - - return items; + return r; } /** @@ -3213,8 +3309,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseOptionalList( kind: K, parseItem: ParseListItem - ): T[] { - return token() === kind.open ? parseList(kind, parseItem) : []; + ): ListDetail { + return token() === kind.open ? parseList(kind, parseItem) : createEmptyList(); } function parseOptionalDelimiter(kind: ListKind) { @@ -3663,6 +3759,10 @@ export function visitChildren(node: Node, cb: NodeCallback): T | undefined return ( visitNode(cb, node.tagName) || visitNode(cb, node.paramName) || visitEach(cb, node.content) ); + case SyntaxKind.DocPropTag: + return ( + visitNode(cb, node.tagName) || visitNode(cb, node.propName) || visitEach(cb, node.content) + ); case SyntaxKind.DocReturnsTag: case SyntaxKind.DocErrorsTag: case SyntaxKind.DocUnknownTag: @@ -3734,25 +3834,68 @@ function visitEach(cb: NodeCallback, nodes: readonly Node[] | undefined): return; } +/** + * check whether a position belongs to a range (excluding the start and end pos) + * i.e. {...} + * + * remark: if range.pos is -1 means no start point found, so return false + * if range.end is -1 means no end point found, so return true if position is greater than range.pos + */ +export function positionInRange(position: number, range: TextRange) { + return range.pos >= 0 && position > range.pos && (range.end === -1 || position < range.end); +} + export function getNodeAtPositionDetail( script: TypeSpecScriptNode, position: number, - filter?: (node: Node) => boolean -): PositionDetail | undefined { - const node = getNodeAtPosition(script, position, filter); - if (!node) return undefined; + filter: (node: Node, flag: "cur" | "pre" | "post") => boolean = () => true +): PositionDetail { + const cur = getNodeAtPosition(script, position, (n) => filter(n, "cur")); + + const input = script.file.text; + const char = input.charCodeAt(position); + const preChar = position >= 0 ? input.charCodeAt(position - 1) : NaN; + const nextChar = position < input.length ? input.charCodeAt(position + 1) : NaN; + + let inTrivia = false; + let triviaStart: number | undefined; + let triviaEnd: number | undefined; + if (!cur || cur.kind !== SyntaxKind.StringLiteral) { + const { char: cp } = codePointBefore(input, position); + if (!cp || !isIdentifierContinue(cp)) { + triviaEnd = skipTrivia(input, position); + triviaStart = skipTriviaBackward(script, position) + 1; + inTrivia = triviaEnd !== position; + } + } - const char = script.file.text.charCodeAt(position); - const preChar = position >= 0 ? script.file.text.charCodeAt(position - 1) : NaN; - const nextChar = - position < script.file.text.length ? script.file.text.charCodeAt(position + 1) : NaN; + if (!inTrivia) { + const beforeId = skipContinuousIdentifier(input, position, true /*isBackward*/); + triviaStart = skipTriviaBackward(script, beforeId) + 1; + const afterId = skipContinuousIdentifier(input, position, false /*isBackward*/); + triviaEnd = skipTrivia(input, afterId); + } + + if (triviaStart === undefined || triviaEnd === undefined) { + compilerAssert(false, "unexpected, triviaStart and triviaEnd should be defined"); + } return { - node, - position, + node: cur, + char, preChar, nextChar, - char, + position, + inTrivia, + triviaStartPosition: triviaStart, + triviaEndPosition: triviaEnd, + getPositionDetailBeforeTrivia: () => { + // getNodeAtPosition will also include the 'node.end' position which is the triviaStart pos + return getNodeAtPositionDetail(script, triviaStart, (n) => filter(n, "pre")); + }, + getPositionDetailAfterTrivia: () => { + return getNodeAtPositionDetail(script, triviaEnd, (n) => filter(n, "post")); + }, }; } @@ -3857,7 +4000,14 @@ function isBlocklessNamespace(node: Node) { return node.statements === undefined; } -export function getFirstAncestor(node: Node, test: NodeCallback): Node | undefined { +export function getFirstAncestor( + node: Node, + test: NodeCallback, + includeSelf: boolean = false +): Node | undefined { + if (includeSelf && test(node)) { + return node; + } for (let n = node.parent; n; n = n.parent) { if (test(n)) { return n; diff --git a/packages/compiler/src/core/program.ts b/packages/compiler/src/core/program.ts index 19ddea2e8a6..719ea13848f 100644 --- a/packages/compiler/src/core/program.ts +++ b/packages/compiler/src/core/program.ts @@ -21,7 +21,7 @@ import { } from "./entrypoint-resolution.js"; import { ExternalError } from "./external-error.js"; import { getLibraryUrlsLoaded } from "./library.js"; -import { createLinter } from "./linter.js"; +import { createLinter, resolveLinterDefinition } from "./linter.js"; import { createLogger } from "./logger/index.js"; import { createTracer } from "./logger/tracer.js"; import { createDiagnostic } from "./messages.js"; @@ -38,6 +38,7 @@ import { getDirectoryPath, joinPaths, resolvePath } from "./path-utils.js"; import { createProjector } from "./projector.js"; import { createSourceFile } from "./source-file.js"; import { StateMap, StateSet, createStateAccessors } from "./state-accessors.js"; +import { createTypeFactory } from "./type-factory.js"; import { CompilerHost, Diagnostic, @@ -92,6 +93,7 @@ export interface Program { host: CompilerHost; tracer: Tracer; trace(area: string, message: string): void; + typeFactory: ReturnType; checker: Checker; emitters: EmitterRef[]; readonly diagnostics: readonly Diagnostic[]; @@ -192,6 +194,7 @@ export async function compile( resolveTypeReference, getSourceFileLocationContext, projectRoot: getDirectoryPath(options.config ?? resolvedMain ?? ""), + typeFactory: undefined!, // todo: check }; trace("compiler.options", JSON.stringify(options, null, 2)); @@ -253,6 +256,7 @@ export async function compile( } program.checker = createChecker(program); program.checker.checkProgram(); + program.typeFactory = createTypeFactory(program); if (!continueToNextStage) { return program; @@ -580,12 +584,13 @@ export async function compile( const libDefinition: TypeSpecLibrary | undefined = entrypoint?.esmExports.$lib; const metadata = computeLibraryMetadata(module, libDefinition); - + // eslint-disable-next-line deprecation/deprecation + const linterDef = entrypoint?.esmExports.$linter ?? libDefinition?.linter; return { ...resolution, metadata, definition: libDefinition, - linter: entrypoint?.esmExports.$linter, + linter: linterDef && resolveLinterDefinition(libraryNameOrPath, linterDef), }; } diff --git a/packages/compiler/src/core/realm.ts b/packages/compiler/src/core/realm.ts new file mode 100644 index 00000000000..335b6deabda --- /dev/null +++ b/packages/compiler/src/core/realm.ts @@ -0,0 +1,167 @@ +import { compilerAssert } from "../index.js"; +import { Program } from "./program.js"; +import { createTypeFactory } from "./type-factory.js"; +import { Type } from "./types.js"; + +class StateMapRealmView implements Map { + #realm: Realm; + #parentState: Map; + #realmState: Map; + + public constructor(realm: Realm, realmState: Map, parentState: Map) { + this.#realm = realm; + this.#parentState = parentState; + this.#realmState = realmState; + } + + has(t: Type) { + return this.dispatch(t).has(t) ?? false; + } + + set(t: Type, v: any) { + this.dispatch(t).set(t, v); + return this; + } + + get(t: Type) { + return this.dispatch(t).get(t); + } + + delete(t: Type) { + return this.dispatch(t).delete(t); + } + + forEach(cb: (value: V, key: Type, map: Map) => void, thisArg?: any) { + for (const item of this.entries()) { + cb.call(thisArg, item[1], item[0], this); + } + + return this; + } + + get size() { + // extremely non-optimal, maybe worth not offering it? + return [...this.entries()].length; + } + + clear() { + this.#realmState.clear(); + } + + *entries() { + for (const item of this.#realmState) { + yield item; + } + + for (const item of this.#parentState) { + yield item; + } + } + + *values() { + for (const item of this.entries()) { + yield item[1]; + } + } + + *keys() { + for (const item of this.entries()) { + yield item[0]; + } + } + + [Symbol.iterator]() { + return this.entries(); + } + + [Symbol.toStringTag] = "StateMap"; + + dispatch(keyType: Type): Map { + if (this.#realm.hasType(keyType)) { + return this.#realmState; + } + + return this.#parentState; + } +} + +export class Realm { + #program!: Program; + + // Type registry + + /** + * Stores all types owned by this realm. + */ + #types = new Set(); + + /** + * Stores types that are deleted in this realm. When a realm is active and doing a traversal, you will + * not find this type in e.g. collections. Deleted types are mapped to `null` if you ask for it. + */ + #deletedTypes = new Set(); + + /** + * Stores types that are not present in the parent realm. + */ + #createdTypes = new Set(); + + #stateMaps = new Map>(); + public key!: symbol; + public typeFactory: ReturnType; + + constructor(program: Program, description: string) { + this.key = Symbol(description); + this.#program = program; + Realm.#knownRealms.set(this.key, this); + this.typeFactory = createTypeFactory(program, this); + } + + stateMap(stateKey: symbol) { + let m = this.#stateMaps.get(stateKey); + + if (!m) { + m = new Map(); + this.#stateMaps.set(stateKey, m); + } + + return new StateMapRealmView(this, m, this.#program.stateMap(stateKey)); + } + + clone(type: T): T { + compilerAssert(type, "Undefined type passed to clone"); + + const clone = this.#cloneIntoRealm(type); + this.typeFactory.finishType(clone); + + return clone; + } + + remove(type: Type): void { + this.#deletedTypes.add(type); + } + + hasType(type: Type): boolean { + return this.#types.has(type); + } + + addType(type: Type): void { + this.#types.add(type); + Realm.realmForType.set(type, this); + } + + #cloneIntoRealm(type: T): T { + const clone = this.typeFactory.initializeClone(type); + this.#types.add(clone); + Realm.realmForType.set(clone, this); + return clone; + } + + static #knownRealms = new Map(); + + static realmForKey(key: symbol, parentRealm?: Realm) { + return this.#knownRealms.get(key); + } + + static realmForType = new Map(); +} diff --git a/packages/compiler/src/core/scanner.ts b/packages/compiler/src/core/scanner.ts index 74fd4d5528c..6e96a5afc1c 100644 --- a/packages/compiler/src/core/scanner.ts +++ b/packages/compiler/src/core/scanner.ts @@ -1,5 +1,6 @@ import { CharCode, + codePointBefore, isAsciiIdentifierContinue, isAsciiIdentifierStart, isBinaryDigit, @@ -17,8 +18,9 @@ import { } from "./charcode.js"; import { DiagnosticHandler, compilerAssert } from "./diagnostics.js"; import { CompilerDiagnostics, createDiagnostic } from "./messages.js"; +import { getCommentAtPosition } from "./parser-utils.js"; import { createSourceFile } from "./source-file.js"; -import { DiagnosticReport, SourceFile, TextRange } from "./types.js"; +import { DiagnosticReport, SourceFile, TextRange, TypeSpecScriptNode } from "./types.js"; // All conflict markers consist of the same character repeated seven times. If it is // a <<<<<<< or >>>>>>> marker then it is also followed by a space. @@ -459,6 +461,8 @@ export function createScanner( return getStringTokenValue(token, tokenFlags); case Token.Identifier: return getIdentifierTokenValue(); + case Token.DocText: + return getDocTextValue(); default: return getTokenText(); } @@ -656,6 +660,10 @@ export function createScanner( case CharCode.LineFeed: return next(Token.NewLine); + case CharCode.Backslash: + tokenFlags |= TokenFlags.Escaped; + return position === endPosition - 1 ? next(Token.DocText) : next(Token.DocText, 2); + case CharCode.Space: case CharCode.Tab: case CharCode.VerticalTab: @@ -1036,6 +1044,44 @@ export function createScanner( return text; } + function getDocTextValue(): string { + if (tokenFlags & TokenFlags.Escaped) { + let start = tokenPosition; + const end = position; + + let result = ""; + let pos = start; + + while (pos < end) { + const ch = input.charCodeAt(pos); + if (ch !== CharCode.Backslash) { + pos++; + continue; + } + + if (pos === end - 1) { + break; + } + + result += input.substring(start, pos); + switch (input.charCodeAt(pos + 1)) { + case CharCode.At: + result += "@"; + break; + default: + result += input.substring(pos, pos + 2); + } + pos += 2; + start = pos; + } + + result += input.substring(start, end); + return result; + } else { + return input.substring(tokenPosition, position); + } + } + function findTripleQuotedStringIndent(start: number, end: number): [number, number] { end = end - 3; // Remove the """ // remove whitespace before closing delimiter and record it as required @@ -1231,6 +1277,8 @@ export function createScanner( return "\\"; case CharCode.$: return "$"; + case CharCode.At: + return "@"; case CharCode.Backtick: return "`"; default: @@ -1372,7 +1420,54 @@ export function createScanner( } } +/** + * + * @param script + * @param position + * @param endPosition exclude + * @returns return === endPosition (or -1) means not found non-trivia until endPosition + 1 + */ +export function skipTriviaBackward( + script: TypeSpecScriptNode, + position: number, + endPosition = -1 +): number { + endPosition = endPosition < -1 ? -1 : endPosition; + const input = script.file.text; + if (position === input.length) { + // it's possible if the pos is at the end of the file, just treat it as trivia + position--; + } else if (position > input.length) { + compilerAssert(false, "position out of range"); + } + + while (position > endPosition) { + const ch = input.charCodeAt(position); + + if (isWhiteSpace(ch)) { + position--; + } else { + const comment = getCommentAtPosition(script, position); + if (comment) { + position = comment.pos - 1; + } else { + break; + } + } + } + + return position; +} + +/** + * + * @param input + * @param position + * @param endPosition exclude + * @returns return === endPosition (or input.length) means not found non-trivia until endPosition - 1 + */ export function skipTrivia(input: string, position: number, endPosition = input.length): number { + endPosition = endPosition > input.length ? input.length : endPosition; while (position < endPosition) { const ch = input.charCodeAt(position); @@ -1450,6 +1545,20 @@ function skipMultiLineComment( return [position, false]; } +export function skipContinuousIdentifier(input: string, position: number, isBackward = false) { + let cur = position; + const direction = isBackward ? -1 : 1; + const bar = isBackward ? (p: number) => p >= 0 : (p: number) => p < input.length; + while (bar(cur)) { + const { char: cp, size } = codePointBefore(input, cur); + cur += direction * size; + if (!cp || !isIdentifierContinue(cp)) { + break; + } + } + return cur; +} + function isConflictMarker(input: string, position: number, endPosition = input.length): boolean { // Conflict markers must be at the start of a line. const ch = input.charCodeAt(position); diff --git a/packages/compiler/src/core/type-factory.ts b/packages/compiler/src/core/type-factory.ts new file mode 100644 index 00000000000..e1eed20656c --- /dev/null +++ b/packages/compiler/src/core/type-factory.ts @@ -0,0 +1,312 @@ +import { createRekeyableMap } from "../utils/misc.js"; +import { Numeric } from "./numeric.js"; +import { Program } from "./program.js"; +import { Realm } from "./realm.js"; +import { + BooleanLiteral, + DecoratorApplication, + DecoratorFunction, + Model, + ModelProperty, + Namespace, + NumericLiteral, + RekeyableMap, + Scalar, + StringLiteral, + Type, + Union, + UnionVariant, +} from "./types.js"; +type DecoratorArgs = DecoratorFunction | [DecoratorFunction, ...any[]]; + +interface ScalarOptions { + extends?: Scalar; + namespace?: Namespace; +} + +interface ModelOptions { + extends?: Model; + namespace?: Namespace; +} + +interface ModelPropertyOptions { + optional?: boolean; +} + +interface UnionVariantOptions { + union?: Union; +} + +export function createTypeFactory(program: Program, realm?: Realm) { + const nostdlib = program.compilerOptions.nostdlib; + const F = { + literal(value: string | number | boolean | null) { + switch (typeof value) { + case "string": + return this.stringLiteral(value); + case "boolean": + return this.booleanLiteral(value); + case "number": + return this.numericLiteral(value); + default: + if (value === null) { + return this.null; + } + throw new Error(`Unknown literal type ${typeof value}`); + } + }, + + stringLiteral(value: string): StringLiteral { + return program.checker.createType({ + kind: "String", + value, + }); + }, + booleanLiteral(value: boolean): BooleanLiteral { + return program.checker.createType({ + kind: "Boolean", + value, + }); + }, + numericLiteral(value: number): NumericLiteral { + const valueAsString = String(value); + return program.checker.createType({ + kind: "Number", + value, + valueAsString, + numericValue: Numeric(valueAsString), + }); + }, + + scalar( + ...args: [...DecoratorArgs[], string, ScalarOptions] | [...DecoratorArgs[], string] + ): Scalar { + const opts = extractArgs(args, false); + const type: Scalar = program.checker.createType({ + kind: "Scalar", + decorators: opts.decorators, + name: opts.name!, + derivedScalars: [], + baseScalar: opts.options.extends, + node: undefined as any, //todo: update this type? + constructors: new Map(), + namespace: opts.options.namespace, //todo: should default to global namespace + }); + + finishType(type); + return type; + }, + + model( + ...args: + | [...DecoratorArgs[], string, ModelProperty[], ModelOptions] + | [...DecoratorArgs[], string, ModelProperty[]] + ): Model { + const opts = extractArgs(args); + + const model: Model = program.checker.createType({ + kind: "Model", + name: opts.name!, + decorators: opts.decorators, + properties: createRekeyableMap(opts.body.map((p) => [p.name, p])), + indexer: undefined, + baseModel: opts.options.extends, + namespace: opts.options.namespace, + node: undefined as any, + derivedModels: [], + sourceModels: [], + }); + + finishType(model); + + return model; + }, + + modelProperty( + ...args: + | [...DecoratorArgs[], string, Type, ModelPropertyOptions] + | [...DecoratorArgs[], string, Type] + ): ModelProperty { + const opts = extractArgs(args); + const property: ModelProperty = program.checker.createType({ + kind: "ModelProperty", + name: opts.name!, + decorators: opts.decorators, + type: opts.body, + node: undefined as any, + optional: !!opts.options.optional, + }); + + finishType(property); + return property; + }, + + union(...args: [...DecoratorArgs[], Type[] | UnionVariant[]]): Union { + const opts = extractArgs(args); + + const union: Union = program.checker.createType({ + kind: "Union", + name: opts.name, + decorators: opts.decorators, + variants: createRekeyableMap(), + get options() { + return Array.from(this.variants.values()).map((v) => v.type); + }, + expression: opts.name === undefined, + node: undefined as any, + }); + + if (opts.body[0].kind === "UnionVariant") { + for (const variant of opts.body as UnionVariant[]) { + union.variants.set(variant.name, variant); + variant.union = union; + } + } else { + for (const variantType of opts.body as Type[]) { + const variant = this.unionVariant(variantType); + variant.union = union; + union.variants.set(variant.name, variant); + } + } + + finishType(union); + + return union; + }, + + unionVariant( + ...args: + | [...DecoratorArgs[], string | symbol, Type, UnionVariantOptions] + | [...DecoratorArgs[], Type, UnionVariantOptions] + | [...DecoratorArgs[], string | symbol, Type] + | [...DecoratorArgs[], Type] + ): UnionVariant { + const opts = extractArgs(args); + const type: UnionVariant = program.checker.createType({ + kind: "UnionVariant", + name: opts.name ?? Symbol("name"), + decorators: opts.decorators, + type: opts.body, + node: undefined as any, // todo, fix node? + union: opts.options.union as any, + }); + finishType(type); + + return type; + }, + + initializeClone(type: T): T { + let clone: T; + switch (type.kind) { + case "Model": + clone = program.checker.createType({ + ...type, + decorators: [...type.decorators], + properties: copyMap(type.properties), + indexer: type.indexer + ? { + ...type.indexer, + } + : undefined, + }); + break; + + case "Union": + clone = program.checker.createType({ + ...type, + decorators: [...type.decorators], + variants: copyMap(type.variants), + get options() { + return Array.from(this.variants.values()).map((v: any) => v.type); + }, + }); + break; + + case "Interface": + clone = program.checker.createType({ + ...type, + decorators: [...type.decorators], + operations: copyMap(type.operations), + }); + break; + + case "Enum": + clone = program.checker.createType({ + ...type, + decorators: [...type.decorators], + members: copyMap(type.members), + }); + break; + default: + clone = program.checker.createType({ + ...type, + ...("decorators" in type ? { decorators: [...type.decorators] } : {}), + }); + break; + } + + realm?.addType(clone); + return clone; + }, + + finishType(type: Type) { + finishType(type); + }, + null: nostdlib ? (null as any) : program.checker.nullType, + never: nostdlib ? (null as any) : program.checker.neverType, + string: nostdlib ? (null as any) : program.checker.getStdType("string"), + globalNamespace: program.getGlobalNamespaceType(), + }; + + return F; + + function finishType(type: Type) { + program.checker.finishType(type, realm); + } + + function copyMap(map: RekeyableMap): RekeyableMap { + return createRekeyableMap(Array.from(map.entries())); + } + + function extractArgs( + args: any[], + noBody = false + ): { + decorators: DecoratorApplication[]; + name: string | undefined; + body: TBody; + options: TOptions; + } { + let index = 0; + const decoratorArgs = takeWhile( + (arg) => typeof arg === "function" || (Array.isArray(arg) && typeof arg[0] === "function") + ); + const decorators: DecoratorApplication[] = []; + for (const arg of decoratorArgs) { + decorators.push({ + decorator: arg[0], + args: arg.slice(1).map((rawValue: any) => ({ + value: typeof rawValue === "object" && rawValue !== null ? rawValue : F.literal(rawValue), + jsValue: rawValue, + })), + }); + } + + return { + decorators, + name: takeWhile((arg) => typeof arg === "string" || typeof arg === "symbol")[0], + body: takeWhile((arg) => true && !noBody)[0], + options: takeWhile((arg) => true)[0] ?? {}, + }; + + function takeWhile(predicate: (arg: any) => boolean) { + const output: any[] = []; + const item = args[index]; + if (predicate(item)) { + output.push(item); + index++; + } + return output; + } + } +} diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index acce9553f97..52f6876abbc 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -949,6 +949,7 @@ export enum SyntaxKind { Doc, DocText, DocParamTag, + DocPropTag, DocReturnsTag, DocErrorsTag, DocTemplateTag, @@ -1043,6 +1044,7 @@ export interface BaseNode extends TextRange { export interface TemplateDeclarationNode { readonly templateParameters: readonly TemplateParameterDeclarationNode[]; + readonly templateParametersRange: TextRange; readonly locals?: SymbolTable; } @@ -1050,11 +1052,35 @@ export interface TemplateDeclarationNode { * owner node and other related information according to the position */ export interface PositionDetail { - readonly node: Node; + readonly node: Node | undefined; readonly position: number; readonly char: number; readonly preChar: number; readonly nextChar: number; + readonly inTrivia: boolean; + + /** + * if the position is in a trivia, return the start position of the trivia containing the position + * if the position is not a trivia, return the start position of the trivia before the text(identifier code) containing the position + * + * Please be aware that this may not be the pre node in the tree because some non-trivia char is ignored in the tree but will counted here + * + * also comments are considered as trivia + */ + readonly triviaStartPosition: number; + /** + * if the position is in a trivia, return the end position (exclude as other 'end' means) of the trivia containing the position + * if the position is not a trivia, return the end position (exclude as other 'end' means) of the trivia after the node containing the position + * + * Please be aware that this may not be the next node in the tree because some non-trivia char is ignored in the tree but will considered here + * + * also comments are considered as trivia + */ + readonly triviaEndPosition: number; + /** get the PositionDetail of positionBeforeTrivia */ + readonly getPositionDetailBeforeTrivia: () => PositionDetail; + /** get the PositionDetail of positionAfterTrivia */ + readonly getPositionDetailAfterTrivia: () => PositionDetail; } export type Node = @@ -1126,7 +1152,8 @@ export type MemberContainerNode = | ModelExpressionNode | InterfaceStatementNode | EnumStatementNode - | UnionStatementNode; + | UnionStatementNode + | ScalarStatementNode; export type MemberNode = | ModelPropertyNode @@ -1140,7 +1167,7 @@ export type MemberContainerType = Model | Enum | Interface | Union | Scalar; /** * Type that can be used as members of a container type. */ -export type MemberType = ModelProperty | EnumMember | Operation | UnionVariant; +export type MemberType = ModelProperty | EnumMember | Operation | UnionVariant | ScalarConstructor; export type Comment = LineComment | BlockComment; @@ -1358,6 +1385,7 @@ export interface OperationStatementNode extends BaseNode, DeclarationNode, Templ export interface ModelStatementNode extends BaseNode, DeclarationNode, TemplateDeclarationNode { readonly kind: SyntaxKind.ModelStatement; readonly properties: readonly (ModelPropertyNode | ModelSpreadPropertyNode)[]; + readonly bodyRange: TextRange; readonly extends?: Expression; readonly is?: Expression; readonly decorators: readonly DecoratorExpressionNode[]; @@ -1369,6 +1397,7 @@ export interface ScalarStatementNode extends BaseNode, DeclarationNode, Template readonly extends?: TypeReferenceNode; readonly decorators: readonly DecoratorExpressionNode[]; readonly members: readonly ScalarConstructorNode[]; + readonly bodyRange: TextRange; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; } @@ -1382,6 +1411,7 @@ export interface ScalarConstructorNode extends BaseNode { export interface InterfaceStatementNode extends BaseNode, DeclarationNode, TemplateDeclarationNode { readonly kind: SyntaxKind.InterfaceStatement; readonly operations: readonly OperationStatementNode[]; + readonly bodyRange: TextRange; readonly extends: readonly TypeReferenceNode[]; readonly decorators: readonly DecoratorExpressionNode[]; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; @@ -1452,6 +1482,7 @@ export interface EmptyStatementNode extends BaseNode { export interface ModelExpressionNode extends BaseNode { readonly kind: SyntaxKind.ModelExpression; readonly properties: (ModelPropertyNode | ModelSpreadPropertyNode)[]; + readonly bodyRange: TextRange; } export interface ArrayExpressionNode extends BaseNode { @@ -1482,6 +1513,7 @@ export interface ModelSpreadPropertyNode extends BaseNode { export interface ObjectLiteralNode extends BaseNode { readonly kind: SyntaxKind.ObjectLiteral; readonly properties: (ObjectLiteralPropertyNode | ObjectLiteralSpreadPropertyNode)[]; + readonly bodyRange: TextRange; } export interface ObjectLiteralPropertyNode extends BaseNode { @@ -1903,6 +1935,7 @@ export type DocTag = | DocReturnsTagNode | DocErrorsTagNode | DocParamTagNode + | DocPropTagNode | DocTemplateTagNode | DocUnknownTagNode; export type DocContent = DocTextNode; @@ -1925,6 +1958,11 @@ export interface DocParamTagNode extends DocTagBaseNode { readonly paramName: IdentifierNode; } +export interface DocPropTagNode extends DocTagBaseNode { + readonly kind: SyntaxKind.DocPropTag; + readonly propName: IdentifierNode; +} + export interface DocTemplateTagNode extends DocTagBaseNode { readonly kind: SyntaxKind.DocTemplateTag; readonly paramName: IdentifierNode; @@ -2047,7 +2085,7 @@ export interface LibraryInstance { entrypoint: JsSourceFileNode | undefined; metadata: LibraryMetadata; definition?: TypeSpecLibrary; - linter: LinterDefinition; + linter: LinterResolvedDefinition; } export type LibraryMetadata = FileLibraryMetadata | ModuleLibraryMetadata; @@ -2437,6 +2475,13 @@ export interface LinterDefinition { ruleSets?: Record; } +export interface LinterResolvedDefinition { + readonly rules: LinterRule[]; + readonly ruleSets: { + [name: string]: LinterRuleSet; + }; +} + export interface LinterRuleDefinition { /** Rule name (without the library name) */ name: N; diff --git a/packages/compiler/src/emitter-framework/index.ts b/packages/compiler/src/emitter-framework/index.ts index c03846896af..7cad2d99667 100644 --- a/packages/compiler/src/emitter-framework/index.ts +++ b/packages/compiler/src/emitter-framework/index.ts @@ -2,6 +2,7 @@ export * from "./asset-emitter.js"; export * from "./builders/array-builder.js"; export * from "./builders/object-builder.js"; export * from "./builders/string-builder.js"; +export * from "./custom-key-map.js"; export * from "./placeholder.js"; export { ReferenceCycle } from "./reference-cycle.js"; export * from "./type-emitter.js"; diff --git a/packages/compiler/src/formatter/print/printer.ts b/packages/compiler/src/formatter/print/printer.ts index 50efdb47c84..c982c709bef 100644 --- a/packages/compiler/src/formatter/print/printer.ts +++ b/packages/compiler/src/formatter/print/printer.ts @@ -1,6 +1,11 @@ import type { AstPath, Doc, Printer } from "prettier"; import { builders } from "prettier/doc"; -import { isIdentifierContinue, isIdentifierStart, utf16CodeUnits } from "../../core/charcode.js"; +import { + CharCode, + isIdentifierContinue, + isIdentifierStart, + utf16CodeUnits, +} from "../../core/charcode.js"; import { compilerAssert } from "../../core/diagnostics.js"; import { Keywords } from "../../core/scanner.js"; import { @@ -86,7 +91,19 @@ import { commentHandler } from "./comment-handler.js"; import { needsParens } from "./needs-parens.js"; import { DecorableNode, PrettierChildPrint, TypeSpecPrettierOptions } from "./types.js"; import { util } from "./util.js"; -const { align, breakParent, group, hardline, ifBreak, indent, join, line, softline } = builders; +const { + align, + breakParent, + group, + hardline, + ifBreak, + indent, + join, + line, + softline, + literalline, + markAsRoot, +} = builders; const { isNextLineEmpty } = util as any; @@ -362,6 +379,7 @@ export function printNode( return printDoc(path as AstPath, options, print); case SyntaxKind.DocText: case SyntaxKind.DocParamTag: + case SyntaxKind.DocPropTag: case SyntaxKind.DocTemplateTag: case SyntaxKind.DocReturnsTag: case SyntaxKind.DocErrorsTag: @@ -682,13 +700,16 @@ function printCallOrDecoratorArgs( } // So that decorator with single object arguments have ( and { hugging. - // @deco({ + // @deco(#{ // value: "foo" // }) const shouldHug = node.arguments.length === 1 && (node.arguments[0].kind === SyntaxKind.ModelExpression || - node.arguments[0].kind === SyntaxKind.StringLiteral); + node.arguments[0].kind === SyntaxKind.ObjectLiteral || + node.arguments[0].kind === SyntaxKind.ArrayLiteral || + node.arguments[0].kind === SyntaxKind.StringLiteral || + node.arguments[0].kind === SyntaxKind.StringTemplateExpression); if (shouldHug) { return [ @@ -1636,7 +1657,28 @@ function printStringLiteral( options: TypeSpecPrettierOptions ): Doc { const node = path.node; - return getRawText(node, options); + const multiline = isMultiline(node, options); + + const raw = getRawText(node, options); + if (multiline) { + const lines = splitLines(raw.slice(3)); + const whitespaceIndent = lines[lines.length - 1].length - 3; + const newLines = trimMultilineString(lines, whitespaceIndent); + return [`"""`, indent(markAsRoot(newLines))]; + } else { + return raw; + } +} + +function isMultiline( + node: StringLiteralNode | StringTemplateExpressionNode, + options: TypeSpecPrettierOptions +) { + return ( + options.originalText[node.pos] && + options.originalText[node.pos + 1] === `"` && + options.originalText[node.pos + 2] === `"` + ); } function printNumberLiteral( @@ -1870,14 +1912,79 @@ export function printStringTemplateExpression( print: PrettierChildPrint ) { const node = path.node; - const content = [ - getRawText(node.head, options), - path.map((span: AstPath) => { - const expression = span.call(print, "expression"); - return [expression, getRawText(span.node.literal, options)]; - }, "spans"), - ]; - return content; + const multiline = isMultiline(node, options); + const rawHead = getRawText(node.head, options); + if (multiline) { + const lastSpan = node.spans[node.spans.length - 1]; + const lastLines = splitLines(getRawText(lastSpan.literal, options)); + const whitespaceIndent = lastLines[lastLines.length - 1].length - 3; + const content = [ + trimMultilineString(splitLines(rawHead.slice(3)), whitespaceIndent), + path.map((span: AstPath) => { + const expression = span.call(print, "expression"); + const spanRawText = getRawText(span.node.literal, options); + const spanLines = splitLines(spanRawText); + return [ + expression, + spanLines[0], + literalline, + trimMultilineString(spanLines.slice(1), whitespaceIndent), + ]; + }, "spans"), + ]; + + return [`"""`, indent(markAsRoot([content]))]; + } else { + const content = [ + rawHead, + path.map((span: AstPath) => { + const expression = span.call(print, "expression"); + return [expression, getRawText(span.node.literal, options)]; + }, "spans"), + ]; + return content; + } +} + +function splitLines(text: string): string[] { + const lines = []; + let start = 0; + let pos = 0; + + while (pos < text.length) { + const ch = text.charCodeAt(pos); + switch (ch) { + case CharCode.CarriageReturn: + if (text.charCodeAt(pos + 1) === CharCode.LineFeed) { + lines.push(text.slice(start, pos)); + start = pos; + pos++; + } else { + lines.push(text.slice(start, pos)); + start = pos; + } + break; + case CharCode.LineFeed: + lines.push(text.slice(start, pos)); + start = pos; + break; + } + pos++; + } + + lines.push(text.slice(start)); + return lines; +} + +function trimMultilineString(lines: string[], whitespaceIndent: number): Doc[] { + const newLines = []; + for (let i = 0; i < lines.length; i++) { + newLines.push(lines[i].slice(whitespaceIndent)); + if (i < lines.length - 1) { + newLines.push(literalline); + } + } + return newLines; } function printItemList( diff --git a/packages/compiler/src/lib/decorators.ts b/packages/compiler/src/lib/decorators.ts index 0ea509d9e6e..124fd3ceaae 100644 --- a/packages/compiler/src/lib/decorators.ts +++ b/packages/compiler/src/lib/decorators.ts @@ -31,6 +31,7 @@ import type { VisibilityDecorator, WithDefaultKeyVisibilityDecorator, WithOptionalPropertiesDecorator, + WithPickedPropertiesDecorator, WithUpdateablePropertiesDecorator, WithVisibilityDecorator, WithoutDefaultValuesDecorator, @@ -879,6 +880,29 @@ export const $withoutOmittedProperties: WithoutOmittedPropertiesDecorator = ( filterModelPropertiesInPlace(target, (prop) => !omitNames.has(prop.name)); }; +// -- @withPickedProperties decorator ---------------------- + +export const $withPickedProperties: WithPickedPropertiesDecorator = ( + context: DecoratorContext, + target: Model, + pickedProperties: Type +) => { + // Get the property or properties to pick + const pickedNames = new Set(); + if (pickedProperties.kind === "String") { + pickedNames.add(pickedProperties.value); + } else if (pickedProperties.kind === "Union") { + for (const variant of pickedProperties.variants.values()) { + if (variant.type.kind === "String") { + pickedNames.add(variant.type.value); + } + } + } + + // Remove all properties not picked + filterModelPropertiesInPlace(target, (prop) => pickedNames.has(prop.name)); +}; + // -- @withoutDefaultValues decorator ---------------------- export const $withoutDefaultValues: WithoutDefaultValuesDecorator = ( diff --git a/packages/compiler/src/server/classify.ts b/packages/compiler/src/server/classify.ts index 9693221f8ae..fcd5f32c0fe 100644 --- a/packages/compiler/src/server/classify.ts +++ b/packages/compiler/src/server/classify.ts @@ -292,6 +292,10 @@ export function getSemanticTokens(ast: TypeSpecScriptNode): SemanticToken[] { classifyDocTag(node.tagName, SemanticTokenKind.DocCommentTag); classifyOverride(node.paramName, SemanticTokenKind.Variable); break; + case SyntaxKind.DocPropTag: + classifyDocTag(node.tagName, SemanticTokenKind.DocCommentTag); + classifyOverride(node.propName, SemanticTokenKind.Variable); + break; case SyntaxKind.DocReturnsTag: classifyDocTag(node.tagName, SemanticTokenKind.DocCommentTag); break; diff --git a/packages/compiler/src/server/compile-service.ts b/packages/compiler/src/server/compile-service.ts index fa793b8c3c3..a31b4832f68 100644 --- a/packages/compiler/src/server/compile-service.ts +++ b/packages/compiler/src/server/compile-service.ts @@ -24,7 +24,7 @@ import { doIO, loadFile, resolveTspMain } from "../utils/misc.js"; import { serverOptions } from "./constants.js"; import { FileService } from "./file-service.js"; import { FileSystemCache } from "./file-system-cache.js"; -import { CompileResult, ServerHost } from "./types.js"; +import { CompileResult, ServerHost, ServerLog } from "./types.js"; import { UpdateManger } from "./update-manager.js"; /** @@ -63,7 +63,7 @@ export interface CompileServiceOptions { readonly fileService: FileService; readonly serverHost: ServerHost; readonly compilerHost: CompilerHost; - readonly log: (message: string, details?: unknown) => void; + readonly log: (log: ServerLog) => void; } export function createCompileService({ @@ -100,12 +100,14 @@ export function createCompileService({ const path = await fileService.getPath(document); const mainFile = await getMainFileForDocument(path); const config = await getConfig(mainFile); + log({ level: "debug", message: `config resolved`, detail: config }); const [optionsFromConfig, _] = resolveOptionsFromConfig(config, { cwd: path }); const options: CompilerOptions = { ...optionsFromConfig, ...serverOptions, }; + log({ level: "debug", message: `compiler options resolved`, detail: options }); if (!fileService.upToDate(document)) { return undefined; @@ -122,6 +124,10 @@ export function createCompileService({ if (mainFile !== path && !program.sourceFiles.has(path)) { // If the file that changed wasn't imported by anything from the main // file, retry using the file itself as the main file. + log({ + level: "debug", + message: `target file was not included in compiling, try to compile ${path} as main file directly`, + }); program = await compileProgram(compilerHost, path, options, oldPrograms.get(path)); oldPrograms.set(path, program); } @@ -166,6 +172,10 @@ export function createCompileService({ const lookupDir = entrypointStat.isDirectory() ? mainFile : getDirectoryPath(mainFile); const configPath = await findTypeSpecConfigPath(compilerHost, lookupDir, true); if (!configPath) { + log({ + level: "debug", + message: `can't find path with config file, try to use default config`, + }); return { ...defaultConfig, projectRoot: getDirectoryPath(mainFile) }; } @@ -210,6 +220,7 @@ export function createCompileService({ */ async function getMainFileForDocument(path: string) { if (path.startsWith("untitled:")) { + log({ level: "debug", message: `untitled document treated as its own main file: ${path}` }); return path; } @@ -237,6 +248,10 @@ export function createCompileService({ const tspMain = resolveTspMain(pkg); if (typeof tspMain === "string") { + log({ + level: "debug", + message: `tspMain resolved from package.json (${pkgPath}) as ${tspMain}`, + }); mainFile = tspMain; } @@ -249,6 +264,7 @@ export function createCompileService({ ); if (stat?.isFile()) { + log({ level: "debug", message: `main file found as ${candidate}` }); return candidate; } @@ -256,16 +272,22 @@ export function createCompileService({ if (parentDir === dir) { break; } + log({ + level: "debug", + message: `main file not found in ${dir}, search in parent directory ${parentDir}`, + }); dir = parentDir; } + log({ level: "debug", message: `reached directory root, using ${path} as main file` }); return path; function logMainFileSearchDiagnostic(diagnostic: TypeSpecDiagnostic) { - log( - `Unexpected diagnostic while looking for main file of ${path}`, - formatDiagnostic(diagnostic) - ); + log({ + level: `error`, + message: `Unexpected diagnostic while looking for main file of ${path}`, + detail: formatDiagnostic(diagnostic), + }); } } } diff --git a/packages/compiler/src/server/completion.ts b/packages/compiler/src/server/completion.ts index 5bea884dc30..73a7903c677 100644 --- a/packages/compiler/src/server/completion.ts +++ b/packages/compiler/src/server/completion.ts @@ -10,6 +10,7 @@ import { getDeprecationDetails } from "../core/deprecation.js"; import { CompilerHost, IdentifierNode, + Node, NodeFlags, NodePackage, PositionDetail, @@ -19,6 +20,9 @@ import { SyntaxKind, Type, TypeSpecScriptNode, + compilerAssert, + getFirstAncestor, + positionInRange, } from "../core/index.js"; import { getAnyExtensionFromPath, @@ -40,11 +44,116 @@ export type CompletionContext = { export async function resolveCompletion( context: CompletionContext, - posDetail: PositionDetail | undefined + posDetail: PositionDetail ): Promise { - const node = posDetail?.node; + let node: Node | undefined = posDetail.node; + + if (!node) { + if ( + posDetail.triviaStartPosition === 0 || + !addCompletionByLookingBackward(posDetail, context) + ) { + addKeywordCompletion("root", context.completions); + } + } else { + // look back first to see whether we can get some completion from the previous statement, e.g. `model Foo |` + if (!addCompletionByLookingBackward(posDetail, context)) { + if (posDetail.inTrivia) { + // If we're not immediately after an identifier character, then advance + // the position past any trivia. This is done because a zero-width + // inserted missing identifier that the user is now trying to complete + // starts after the trivia following the cursor. + node = posDetail.getPositionDetailAfterTrivia().node; + } + await AddCompletionNonTrivia(node, context, posDetail); + } else { + if (!posDetail.inTrivia) { + await AddCompletionNonTrivia(node, context, posDetail); + } + } + } + + return context.completions; +} + +function addCompletionByLookingBackward( + posDetail: PositionDetail, + context: CompletionContext +): boolean { + if (posDetail.triviaStartPosition === 0) { + return false; + } + const preDetail = posDetail.getPositionDetailBeforeTrivia(); + if (!preDetail.node) { + return false; + } + + const node = getFirstAncestor( + preDetail.node, + (n) => + n.kind === SyntaxKind.ModelStatement || + n.kind === SyntaxKind.ScalarStatement || + n.kind === SyntaxKind.OperationStatement || + n.kind === SyntaxKind.InterfaceStatement || + n.kind === SyntaxKind.TemplateParameterDeclaration, + true /*includeSelf*/ + ); + + return node !== undefined && addCompletionByLookingBackwardNode(node, posDetail, context); +} + +function addCompletionByLookingBackwardNode( + preNode: Node, + posDetail: PositionDetail, + context: CompletionContext +): boolean { + const getIdentifierEndPos = (n: IdentifierNode) => { + // n.pos === n.end, it means it's a missing identifier, just return -1; + return n.pos === n.end ? -1 : n.end; + }; + const map: { [key in SyntaxKind]?: keyof KeywordArea } = { + [SyntaxKind.ModelStatement]: "modelHeader", + [SyntaxKind.ScalarStatement]: "scalarHeader", + [SyntaxKind.OperationStatement]: "operationHeader", + [SyntaxKind.InterfaceStatement]: "interfaceHeader", + }; + switch (preNode?.kind) { + case SyntaxKind.ModelStatement: + case SyntaxKind.ScalarStatement: + case SyntaxKind.OperationStatement: + case SyntaxKind.InterfaceStatement: + const idEndPos = + preNode.templateParametersRange.end >= 0 + ? preNode.templateParametersRange.end + : getIdentifierEndPos(preNode.id); + if (posDetail.triviaStartPosition === idEndPos) { + const key = map[preNode.kind]; + if (!key) { + compilerAssert(false, "KeywordArea missing in keyarea map."); + } + addKeywordCompletion(key, context.completions); + return true; + } + break; + case SyntaxKind.TemplateParameterDeclaration: + if (posDetail.triviaStartPosition === getIdentifierEndPos(preNode.id)) { + addKeywordCompletion("templateParameter", context.completions); + return true; + } else if (preNode.parent?.templateParametersRange.end === posDetail.triviaStartPosition) { + return addCompletionByLookingBackwardNode(preNode.parent, posDetail, context); + } + break; + } + return false; +} + +async function AddCompletionNonTrivia( + node: Node | undefined, + context: CompletionContext, + posDetail: PositionDetail, + lookBackward: boolean = true +) { if ( - posDetail === undefined || node === undefined || node.kind === SyntaxKind.InvalidStatement || (node.kind === SyntaxKind.Identifier && @@ -58,7 +167,9 @@ export async function resolveCompletion( addKeywordCompletion("namespace", context.completions); break; case SyntaxKind.ScalarStatement: - addKeywordCompletion("scalar", context.completions); + if (positionInRange(posDetail.position, node.bodyRange)) { + addKeywordCompletion("scalarBody", context.completions); + } break; case SyntaxKind.Identifier: addDirectiveCompletion(context, node); @@ -76,16 +187,18 @@ export async function resolveCompletion( break; } } - - return context.completions; } interface KeywordArea { root?: boolean; namespace?: boolean; - model?: boolean; + modelHeader?: boolean; identifier?: boolean; - scalar?: boolean; + scalarHeader?: boolean; + scalarBody?: boolean; + templateParameter?: boolean; + operationHeader?: boolean; + interfaceHeader?: boolean; } const keywords = [ @@ -107,8 +220,11 @@ const keywords = [ ["const", { root: true, namespace: true }], // On model `model Foo ...` - ["extends", { model: true }], - ["is", { model: true }], + [ + "extends", + { modelHeader: true, scalarHeader: true, templateParameter: true, interfaceHeader: true }, + ], + ["is", { modelHeader: true, operationHeader: true }], // On identifier ["true", { identifier: true }], @@ -121,7 +237,7 @@ const keywords = [ ["extern", { root: true, namespace: true }], // Scalars - ["init", { scalar: true }], + ["init", { scalarBody: true }], ] as const; function addKeywordCompletion(area: keyof KeywordArea, completions: CompletionList) { @@ -247,34 +363,37 @@ async function addRelativePathCompletion( function addModelCompletion(context: CompletionContext, posDetail: PositionDetail) { const node = posDetail.node; if ( - node.kind !== SyntaxKind.ModelStatement && - node.kind !== SyntaxKind.ModelExpression && - node.kind !== SyntaxKind.ObjectLiteral + !node || + (node.kind !== SyntaxKind.ModelStatement && + node.kind !== SyntaxKind.ModelExpression && + node.kind !== SyntaxKind.ObjectLiteral) ) { return; } - // skip the scenario like `{ ... }|` - if (node.end === posDetail.position) { + + if (posDetail.position === node.bodyRange.end) { + // skip the scenario like `{ ... }|` return; + } else { + // create a fake identifier node to further resolve the completions for the model/object + // it's a little tricky but can help to keep things clean and simple while the cons. is limited + // TODO: consider adding support in resolveCompletions for non-identifier-node directly when we find more scenario and worth the cost + const fakeProp = { + kind: + node.kind === SyntaxKind.ObjectLiteral + ? SyntaxKind.ObjectLiteralProperty + : SyntaxKind.ModelProperty, + flags: NodeFlags.None, + parent: node, + }; + const fakeId = { + kind: SyntaxKind.Identifier, + sv: "", + flags: NodeFlags.None, + parent: fakeProp, + }; + addIdentifierCompletion(context, fakeId as IdentifierNode); } - // create a fake identifier node to further resolve the completions for the model/object - // it's a little tricky but can help to keep things clean and simple while the cons. is limited - // TODO: consider adding support in resolveCompletions for non-identifier-node directly when we find more scenario and worth the cost - const fakeProp = { - kind: - node.kind === SyntaxKind.ObjectLiteral - ? SyntaxKind.ObjectLiteralProperty - : SyntaxKind.ModelProperty, - flags: NodeFlags.None, - parent: node, - }; - const fakeId = { - kind: SyntaxKind.Identifier, - sv: "", - flags: NodeFlags.None, - parent: fakeProp, - }; - addIdentifierCompletion(context, fakeId as IdentifierNode); } /** diff --git a/packages/compiler/src/server/file-system-cache.ts b/packages/compiler/src/server/file-system-cache.ts index c95132bdfd8..87818e2abba 100644 --- a/packages/compiler/src/server/file-system-cache.ts +++ b/packages/compiler/src/server/file-system-cache.ts @@ -1,6 +1,7 @@ import { FileEvent } from "vscode-languageserver"; import { SourceFile } from "../core/types.js"; import { FileService } from "./file-service.js"; +import { ServerLog } from "./types.js"; export interface FileSystemCache { get(path: string): Promise; @@ -27,8 +28,10 @@ export interface CachedError { } export function createFileSystemCache({ fileService, + log, }: { fileService: FileService; + log: (log: ServerLog) => void; }): FileSystemCache { const cache = new Map(); let changes: FileEvent[] = []; @@ -36,10 +39,21 @@ export function createFileSystemCache({ async get(path: string) { for (const change of changes) { const path = await fileService.fileURLToRealPath(change.uri); + log({ + level: "trace", + message: `FileSystemCache entry with key '${path}' removed`, + }); cache.delete(path); } changes = []; - return cache.get(path); + const r = cache.get(path); + if (!r) { + const target: any = {}; + Error.captureStackTrace(target); + const callstack = target.stack.substring("Error\n".length); + log({ level: "trace", message: `FileSystemCache miss for ${path}`, detail: callstack }); + } + return r; }, set(path: string, entry: CachedFile | CachedError) { cache.set(path, entry); diff --git a/packages/compiler/src/server/server.ts b/packages/compiler/src/server/server.ts index 0c88394529a..48d88a15674 100644 --- a/packages/compiler/src/server/server.ts +++ b/packages/compiler/src/server/server.ts @@ -15,7 +15,7 @@ import { import { NodeHost } from "../core/node-host.js"; import { typespecVersion } from "../utils/misc.js"; import { createServer } from "./serverlib.js"; -import { Server, ServerHost } from "./types.js"; +import { Server, ServerHost, ServerLog } from "./types.js"; let server: Server | undefined = undefined; @@ -45,8 +45,38 @@ function main() { sendDiagnostics(params: PublishDiagnosticsParams) { void connection.sendDiagnostics(params); }, - log(message: string) { - connection.console.log(message); + log(log: ServerLog) { + const message = log.message; + let detail: string | undefined = undefined; + let fullMessage = message; + if (log.detail) { + detail = + typeof log.detail === "string" ? log.detail : JSON.stringify(log.detail, undefined, 2); + fullMessage = `${message}:\n${detail}`; + } + + switch (log.level) { + case "trace": + connection.tracer.log(message, detail); + break; + case "debug": + connection.console.debug(fullMessage); + break; + case "info": + connection.console.info(fullMessage); + break; + case "warning": + connection.console.warn(fullMessage); + break; + case "error": + connection.console.error(fullMessage); + break; + default: + connection.console.error( + `Log Message with invalid LogLevel (${log.level}). Raw Message: ${fullMessage}` + ); + break; + } }, getOpenDocumentByURL(url: string) { return documents.get(url); @@ -58,13 +88,13 @@ function main() { const s = createServer(host); server = s; - s.log(`TypeSpec language server v${typespecVersion}`); - s.log("Module", fileURLToPath(import.meta.url)); - s.log("Process ID", process.pid); - s.log("Command Line", process.argv); + s.log({ level: `info`, message: `TypeSpec language server v${typespecVersion}` }); + s.log({ level: `info`, message: `Module: ${fileURLToPath(import.meta.url)}` }); + s.log({ level: `info`, message: `Process ID: ${process.pid}` }); + s.log({ level: `info`, message: `Command Line`, detail: process.argv }); if (profileDir) { - s.log("CPU profiling enabled", profileDir); + s.log({ level: `info`, message: `CPU profiling enabled with dir: ${profileDir}` }); profileSession = new inspector.Session(); profileSession.connect(); } @@ -152,7 +182,7 @@ function time any>(func: T): T { const start = Date.now(); const ret = await func.apply(undefined!, args); const end = Date.now(); - server!.log(func.name, end - start + " ms"); + server!.log({ level: `trace`, message: `${func.name}: ${end - start + " ms"}` }); return ret; }) as T; } diff --git a/packages/compiler/src/server/serverlib.ts b/packages/compiler/src/server/serverlib.ts index f6a340b5bfa..4dc89f625e7 100644 --- a/packages/compiler/src/server/serverlib.ts +++ b/packages/compiler/src/server/serverlib.ts @@ -45,7 +45,7 @@ import { WorkspaceEdit, WorkspaceFoldersChangeEvent, } from "vscode-languageserver/node.js"; -import { CharCode, codePointBefore, isIdentifierContinue } from "../core/charcode.js"; +import { CharCode } from "../core/charcode.js"; import { resolveCodeFix } from "../core/code-fixes.js"; import { compilerAssert, getSourceLocation } from "../core/diagnostics.js"; import { formatTypeSpec } from "../core/formatter.js"; @@ -92,6 +92,7 @@ import { SemanticTokenKind, Server, ServerHost, + ServerLog, ServerSourceFile, ServerWorkspaceFolder, } from "./types.js"; @@ -105,6 +106,7 @@ export function createServer(host: ServerHost): Server { // a file change. const fileSystemCache = createFileSystemCache({ fileService, + log, }); const compilerHost = createCompilerHost(); @@ -121,7 +123,7 @@ export function createServer(host: ServerHost): Server { let workspaceFolders: ServerWorkspaceFolder[] = []; let isInitialized = false; - let pendingMessages: string[] = []; + let pendingMessages: ServerLog[] = []; return { get pendingMessages() { @@ -236,17 +238,17 @@ export function createServer(host: ServerHost): Server { ]; } - log("Workspace Folders", workspaceFolders); + log({ level: "info", message: `Workspace Folders`, detail: workspaceFolders }); return { capabilities }; } function initialized(params: InitializedParams): void { isInitialized = true; - log("Initialization complete."); + log({ level: "info", message: "Initialization complete." }); } async function workspaceFoldersChanged(e: WorkspaceFoldersChangeEvent) { - log("Workspace Folders Changed", e); + log({ level: "info", message: "Workspace Folders Changed", detail: e }); const map = new Map(workspaceFolders.map((f) => [f.uri, f])); for (const folder of e.removed) { map.delete(folder.uri); @@ -258,7 +260,7 @@ export function createServer(host: ServerHost): Server { }); } workspaceFolders = Array.from(map.values()); - log("Workspace Folders", workspaceFolders); + log({ level: "info", message: `Workspace Folders`, detail: workspaceFolders }); } function watchedFilesChanged(params: DidChangeWatchedFilesParams) { @@ -383,6 +385,7 @@ export function createServer(host: ServerHost): Server { // we report diagnostics with no location on the document that changed to // trigger. diagDocument = document; + log({ level: "debug", message: `Diagnostic with no location: ${each.message}` }); } if (!diagDocument || !fileService.upToDate(diagDocument)) { @@ -883,14 +886,9 @@ export function createServer(host: ServerHost): Server { } } - function log(message: string, details: any = undefined) { - message = `[${new Date().toLocaleTimeString()}] ${message}`; - if (details) { - message += ": " + JSON.stringify(details, undefined, 2); - } - + function log(log: ServerLog) { if (!isInitialized) { - pendingMessages.push(message); + pendingMessages.push(log); return; } @@ -899,7 +897,7 @@ export function createServer(host: ServerHost): Server { } pendingMessages = []; - host.log(message); + host.log(log); } function sendDiagnostics(document: TextDocument, diagnostics: VSDiagnostic[]) { @@ -1077,21 +1075,6 @@ export function getCompletionNodeAtPosition( script: TypeSpecScriptNode, position: number, filter: (node: Node) => boolean = (node: Node) => true -): PositionDetail | undefined { - const detail = getNodeAtPositionDetail(script, position, filter); - if (detail?.node.kind === SyntaxKind.StringLiteral) { - return detail; - } - // If we're not immediately after an identifier character, then advance - // the position past any trivia. This is done because a zero-width - // inserted missing identifier that the user is now trying to complete - // starts after the trivia following the cursor. - const cp = codePointBefore(script.file.text, position); - if (!cp || !isIdentifierContinue(cp)) { - const newPosition = skipTrivia(script.file.text, position); - if (newPosition !== position) { - return getNodeAtPositionDetail(script, newPosition, filter); - } - } - return detail; +): PositionDetail { + return getNodeAtPositionDetail(script, position, filter); } diff --git a/packages/compiler/src/server/tmlanguage.ts b/packages/compiler/src/server/tmlanguage.ts index 37a1ebe181e..a476d7796b3 100644 --- a/packages/compiler/src/server/tmlanguage.ts +++ b/packages/compiler/src/server/tmlanguage.ts @@ -191,13 +191,14 @@ const blockComment: BeginEndRule = { const docCommentParam: MatchRule = { key: "doc-comment-param", scope: "comment.block.tsp", - match: `(?x)((@)(?:param|template))\\s+(${identifier})\\b`, + match: `(?x)((@)(?:param|template|prop))\\s+(${identifier})\\b`, captures: { "1": { scope: "keyword.tag.tspdoc" }, "2": { scope: "keyword.tag.tspdoc" }, "3": { scope: "variable.name.tsp" }, }, }; + const docCommentReturn: MatchRule = { key: "doc-comment-return-tag", scope: "comment.block.tsp", @@ -207,6 +208,7 @@ const docCommentReturn: MatchRule = { "2": { scope: "keyword.tag.tspdoc" }, }, }; + const docCommentUnknownTag: MatchRule = { key: "doc-comment-unknown-tag", scope: "comment.block.tsp", diff --git a/packages/compiler/src/server/types.ts b/packages/compiler/src/server/types.ts index cd3ae32f9cf..d769df6560e 100644 --- a/packages/compiler/src/server/types.ts +++ b/packages/compiler/src/server/types.ts @@ -39,12 +39,19 @@ import { import { TextDocument, TextEdit } from "vscode-languageserver-textdocument"; import type { CompilerHost, Program, SourceFile, TypeSpecScriptNode } from "../core/index.js"; +export type ServerLogLevel = "trace" | "debug" | "info" | "warning" | "error"; +export interface ServerLog { + level: ServerLogLevel; + message: string; + detail?: unknown; +} + export interface ServerHost { readonly compilerHost: CompilerHost; readonly throwInternalErrors?: boolean; readonly getOpenDocumentByURL: (url: string) => TextDocument | undefined; readonly sendDiagnostics: (params: PublishDiagnosticsParams) => void; - readonly log: (message: string) => void; + readonly log: (log: ServerLog) => void; readonly applyEdit: ( paramOrEdit: ApplyWorkspaceEditParams | WorkspaceEdit ) => Promise; @@ -57,7 +64,7 @@ export interface CompileResult { } export interface Server { - readonly pendingMessages: readonly string[]; + readonly pendingMessages: readonly ServerLog[]; readonly workspaceFolders: readonly ServerWorkspaceFolder[]; compile(document: TextDocument | TextDocumentIdentifier): Promise; initialize(params: InitializeParams): Promise; @@ -81,7 +88,7 @@ export interface Server { documentClosed(change: TextDocumentChangeEvent): void; getCodeActions(params: CodeActionParams): Promise; executeCommand(params: ExecuteCommandParams): Promise; - log(message: string, details?: any): void; + log(log: ServerLog): void; } export interface ServerSourceFile extends SourceFile { diff --git a/packages/compiler/src/testing/test-server-host.ts b/packages/compiler/src/testing/test-server-host.ts index d82fe9b698a..d11e4d7ceaf 100644 --- a/packages/compiler/src/testing/test-server-host.ts +++ b/packages/compiler/src/testing/test-server-host.ts @@ -68,8 +68,8 @@ export async function createTestServerHost(options?: TestHostOptions & { workspa } diagnostics.set(params.uri, params.diagnostics); }, - log(message) { - logMessages.push(message); + log(log) { + logMessages.push(`[${log.level}] ${log.message}`); }, getURL(path: string) { if (path.startsWith("untitled:")) { diff --git a/packages/compiler/templates/scaffolding.json b/packages/compiler/templates/scaffolding.json index 3c277e14fa2..869a016ad26 100644 --- a/packages/compiler/templates/scaffolding.json +++ b/packages/compiler/templates/scaffolding.json @@ -3,12 +3,12 @@ "title": "Empty project", "description": "Create an empty project.", "libraries": [], - "compilerVersion": "0.56.0" + "compilerVersion": "0.57.0" }, "rest": { "title": "Generic REST API", "description": "Create a project representing a generic REST API", - "compilerVersion": "0.56.0", + "compilerVersion": "0.57.0", "libraries": [ "@typespec/http", "@typespec/rest", @@ -23,7 +23,7 @@ "library-ts": { "title": "TypeSpec Library (With TypeScript)", "description": "Create a new package to add decorators or linters to typespec.", - "compilerVersion": "0.56.0", + "compilerVersion": "0.57.0", "libraries": [], "files": [ { @@ -99,7 +99,7 @@ "emitter-ts": { "title": "TypeSpec Emitter (With TypeScript)", "description": "Create a new package that will be emitting typespec", - "compilerVersion": "0.56.0", + "compilerVersion": "0.57.0", "libraries": [], "files": [ { diff --git a/packages/compiler/test/checker/doc-comment.test.ts b/packages/compiler/test/checker/doc-comment.test.ts index a1a385d3363..202d5ae23bd 100644 --- a/packages/compiler/test/checker/doc-comment.test.ts +++ b/packages/compiler/test/checker/doc-comment.test.ts @@ -4,12 +4,12 @@ import { Model, Operation } from "../../src/core/index.js"; import { getDoc, getErrorsDoc, getReturnsDoc } from "../../src/lib/decorators.js"; import { BasicTestRunner, createTestRunner } from "../../src/testing/index.js"; -describe("compiler: checker: doc comments", () => { - let runner: BasicTestRunner; - beforeEach(async () => { - runner = await createTestRunner(); - }); +let runner: BasicTestRunner; +beforeEach(async () => { + runner = await createTestRunner(); +}); +describe("compiler: checker: doc comments", () => { const expectedMainDoc = "This is a doc comment."; const docComment = `/** * ${expectedMainDoc} @@ -238,8 +238,114 @@ describe("compiler: checker: doc comments", () => { strictEqual(getErrorsDoc(runner.program, test), "Another string"); }); }); +}); + +describe("@param", () => { + async function getDocForParam(code: string): Promise { + const { target } = (await runner.compile(code)) as { target: Operation }; + ok(target, `Make sure to have @test("target") in code.`); + return getDoc(runner.program, target.parameters.properties.get("one")!); + } + + it("applies doc on param", async () => { + const doc = await getDocForParam(` + /** + * @param one Doc comment + */ + @test("target") op base(one: string): void; + `); + strictEqual(doc, "Doc comment"); + }); + + it("@doc on param wins", async () => { + const doc = await getDocForParam(` + /** + * @param one Doc comment + */ + @test("target") op base(@doc("Explicit") one: string): void; + `); + strictEqual(doc, "Explicit"); + }); + + it("augment @@doc on param wins", async () => { + const doc = await getDocForParam(` + /** + * @param one Doc comment + */ + @test("target") op base(one: string): void; + + @@doc(base::parameters.one, "Override"); + `); + strictEqual(doc, "Override"); + }); + + it("carry over with op is", async () => { + const doc = await getDocForParam(` + /** + * @param one Doc comment + */ + op base(one: string): void; + + @test("target") op child is base; + `); + strictEqual(doc, "Doc comment"); + }); + + it("@param on child operation override parent @param", async () => { + const doc = await getDocForParam(` + /** + * @param one Doc comment + */ + op base(one: string): void; + + /** + * @param one Override for child + */ + @test("target") op child is base; + `); + strictEqual(doc, "Override for child"); + }); + + it("augment @@doc wins over @param on child operation", async () => { + const doc = await getDocForParam(` + /** + * @param one Doc comment + */ + op base(one: string): void; + + /** + * @param one Override for child + */ + @test("target") op child is base; + @@doc(child::parameters.one, "Override for child again"); + `); + strictEqual(doc, "Override for child again"); + }); + + it("spread model without @param keeps doc on property", async () => { + const doc = await getDocForParam(` + model A { + @doc("Via model") one: string + } + @test("target") op base(...A): void; + `); + strictEqual(doc, "Via model"); + }); - it("using @param in doc comment of operation applies doc on the parameters", async () => { + it("@param override doc set from spread model", async () => { + const doc = await getDocForParam(` + model A { + @doc("Via model") one: string + } + /** + * @param one Doc comment + */ + @test("target") op base(...A): void; + `); + strictEqual(doc, "Doc comment"); + }); + + it("applies to distinct parameters", async () => { // One @param has a hyphen but the other does not (should handle both cases) const { addUser } = (await runner.compile(` @@ -262,3 +368,126 @@ describe("compiler: checker: doc comments", () => { ); }); }); + +describe("@prop", () => { + async function getDocForProp(code: string): Promise { + const { target } = (await runner.compile(code)) as { target: Model }; + ok(target, `Make sure to have @test("target") in code.`); + return getDoc(runner.program, target.properties.get("one")!); + } + + it("applies doc on param", async () => { + const doc = await getDocForProp(` + /** + * @prop one Doc comment + */ + @test("target") model Base { one: string } + `); + strictEqual(doc, "Doc comment"); + }); + + it("@doc on param wins", async () => { + const doc = await getDocForProp(` + /** + * @prop one Doc comment + */ + @test("target") model Base { @doc("Explicit") one: string } + `); + strictEqual(doc, "Explicit"); + }); + + it("augment @@doc on param wins", async () => { + const doc = await getDocForProp(` + /** + * @prop one Doc comment + */ + @test("target") model Base { one: string } + + @@doc(Base.one, "Override"); + `); + strictEqual(doc, "Override"); + }); + + it("carry over with model is", async () => { + const doc = await getDocForProp(` + /** + * @prop one Doc comment + */ + model Base { one: string } + + @test("target") model Child is Base; + `); + strictEqual(doc, "Doc comment"); + }); + + it("@prop on child operation override parent @prop", async () => { + const doc = await getDocForProp(` + /** + * @prop one Doc comment + */ + model Base { one: string } + + /** + * @prop one Override for child + */ + @test("target") model Child is Base; + `); + strictEqual(doc, "Override for child"); + }); + + it("augment @@doc wins over @prop on child operation", async () => { + const doc = await getDocForProp(` + /** + * @prop one Doc comment + */ + model Base { one: string } + + /** + * @prop one Override for child + */ + @test("target") model Child is Base; + @@doc(Child.one, "Override for child again"); + `); + strictEqual(doc, "Override for child again"); + }); + + it("spread model without @prop keeps doc on property", async () => { + const doc = await getDocForProp(` + model Base { + @doc("Via model") one: string + } + @test("target") model Child { ...Base } + `); + strictEqual(doc, "Via model"); + }); + + it("@prop override doc set from spread model", async () => { + const doc = await getDocForProp(` + model Base { + @doc("Via model") one: string + } + /** + * @prop one Doc comment + */ + @test("target") model Child { ...Base } + `); + strictEqual(doc, "Doc comment"); + }); + + it("applies to distinct parameters", async () => { + // One @prop has a hyphen but the other does not (should handle both cases) + const { Base } = (await runner.compile(` + + /** + * This is the model doc. + * @prop name This is the name prop doc. + * @prop age - This is the age prop doc. + */ + @test model Base { name: string, age: int32 } + `)) as { Base: Model }; + + strictEqual(getDoc(runner.program, Base), "This is the model doc."); + strictEqual(getDoc(runner.program, Base.properties.get("name")!), "This is the name prop doc."); + strictEqual(getDoc(runner.program, Base.properties.get("age")!), "This is the age prop doc."); + }); +}); diff --git a/packages/compiler/test/checker/operations.test.ts b/packages/compiler/test/checker/operations.test.ts index fd04995c921..743e0460bfe 100644 --- a/packages/compiler/test/checker/operations.test.ts +++ b/packages/compiler/test/checker/operations.test.ts @@ -58,6 +58,19 @@ describe("compiler: operations", () => { ); }); + describe("js special words for parameter names", () => { + it.each(["constructor", "toString"])("%s", async (name) => { + testHost.addTypeSpecFile( + "main.tsp", + ` + @test op a(${name}: string): void; + ` + ); + const { a } = (await testHost.compile("main.tsp")) as { a: Operation }; + ok(a.parameters.properties.has(name)); + }); + }); + it("can decorate operation parameters independently", async () => { testHost.addTypeSpecFile( "main.tsp", diff --git a/packages/compiler/test/checker/values/scalar-values.test.ts b/packages/compiler/test/checker/values/scalar-values.test.ts index cb01e8ea201..aece17ec4d4 100644 --- a/packages/compiler/test/checker/values/scalar-values.test.ts +++ b/packages/compiler/test/checker/values/scalar-values.test.ts @@ -49,6 +49,21 @@ describe("instantiate with named constructor", () => { ]); }); + it("instantiate using constructor from parent scalar", async () => { + const value = await compileValue( + `b.fromString("a")`, + ` + scalar a { init fromString(val: string);} + scalar b extends a { } + ` + ); + strictEqual(value.valueKind, "ScalarValue"); + strictEqual(value.type.kind, "Scalar"); + strictEqual(value.type.name, "b"); + strictEqual(value.scalar?.name, "b"); + strictEqual(value.value.name, "fromString"); + }); + it("instantiate from another scalar", async () => { const value = await compileValue( `b.fromA(a.fromString("a"))`, diff --git a/packages/compiler/test/core/linter.test.ts b/packages/compiler/test/core/linter.test.ts index 4f13b5fe318..66392c6c290 100644 --- a/packages/compiler/test/core/linter.test.ts +++ b/packages/compiler/test/core/linter.test.ts @@ -1,7 +1,7 @@ import { describe, it } from "vitest"; import { createLinterRule, createTypeSpecLibrary } from "../../src/core/library.js"; -import { Linter, createLinter } from "../../src/core/linter.js"; +import { Linter, createLinter, resolveLinterDefinition } from "../../src/core/linter.js"; import type { LibraryInstance, LinterDefinition } from "../../src/index.js"; import { createTestHost, @@ -45,13 +45,13 @@ describe("compiler: linter", () => { const library: LibraryInstance = { entrypoint: undefined, - metadata: { type: "module", name: "@typespec/test" }, + metadata: { type: "module", name: "@typespec/test-linter" }, module: { type: "module", path: "", mainFile: "", manifest: { name: "", version: "" } }, definition: createTypeSpecLibrary({ - name: "@typespec/test", + name: "@typespec/test-linter", diagnostics: {}, }), - linter: linterDef, + linter: resolveLinterDefinition("@typespec/test-linter", linterDef), }; await host.compile("main.tsp"); @@ -212,6 +212,44 @@ describe("compiler: linter", () => { }); }); + describe("when enabling a ruleset", () => { + it("/all ruleset is automatically provided and include all rules", async () => { + const linter = await createTestLinter(`model Foo {}`, { + rules: [noModelFoo], + }); + expectDiagnosticEmpty( + await linter.extendRuleSet({ + extends: ["@typespec/test-linter/all"], + }) + ); + expectDiagnostics(linter.lint(), { + severity: "warning", + code: "@typespec/test-linter/no-model-foo", + message: `Cannot call model 'Foo'`, + }); + }); + it("extending specific ruleset enable the rules inside", async () => { + const linter = await createTestLinter(`model Foo {}`, { + rules: [noModelFoo], + ruleSets: { + custom: { + enable: { "@typespec/test-linter/no-model-foo": true }, + }, + }, + }); + expectDiagnosticEmpty( + await linter.extendRuleSet({ + extends: ["@typespec/test-linter/custom"], + }) + ); + expectDiagnostics(linter.lint(), { + severity: "warning", + code: "@typespec/test-linter/no-model-foo", + message: `Cannot call model 'Foo'`, + }); + }); + }); + describe("(integration) loading in program", () => { async function diagnoseReal(code: string) { const host = await createTestHost(); diff --git a/packages/compiler/test/decorators/decorators.test.ts b/packages/compiler/test/decorators/decorators.test.ts index d9fedf691f9..5cb9b1a9a14 100644 --- a/packages/compiler/test/decorators/decorators.test.ts +++ b/packages/compiler/test/decorators/decorators.test.ts @@ -782,6 +782,43 @@ describe("compiler: built-in decorators", () => { }); }); + describe("@withPickedProperties", () => { + it("picks a model property when given a string literal", async () => { + const { TestModel } = await runner.compile( + ` + model OriginalModel { + pickMe: string; + notMe: string; + } + + @test + model TestModel is PickProperties { + }` + ); + + const properties = TestModel.kind === "Model" ? Array.from(TestModel.properties.keys()) : []; + deepStrictEqual(properties, ["pickMe"]); + }); + + it("picks model properties when given a union containing strings", async () => { + const { TestModel } = await runner.compile( + ` + model OriginalModel { + pickMe: string; + pickMeToo: string; + notMe: string; + } + + @test + model TestModel is PickProperties { + }` + ); + + const properties = TestModel.kind === "Model" ? Array.from(TestModel.properties.keys()) : []; + deepStrictEqual(properties, ["pickMe", "pickMeToo"]); + }); + }); + describe("@withDefaultKeyVisibility", () => { it("sets the default visibility on a key property when not already present", async () => { const { TestModel } = (await runner.compile( diff --git a/packages/compiler/test/formatter/formatter.test.ts b/packages/compiler/test/formatter/formatter.test.ts index 43b03224f81..39b04d7ed80 100644 --- a/packages/compiler/test/formatter/formatter.test.ts +++ b/packages/compiler/test/formatter/formatter.test.ts @@ -852,6 +852,59 @@ scalar Foo { }); }); }); + + describe("scalar constructor call", () => { + it("simple call", async () => { + await assertFormat({ + code: ` +const foo = utcDateTime. fromISO( + "abc" ); +`, + expected: ` +const foo = utcDateTime.fromISO("abc"); +`, + }); + }); + + it("hug object literal", async () => { + await assertFormat({ + code: ` +const foo = utcDateTime. fromFoo(#{ name: "abc", + multiline1: "abc", + multiline2: "abc", + multiline3: "abc", }); +`, + expected: ` +const foo = utcDateTime.fromFoo(#{ + name: "abc", + multiline1: "abc", + multiline2: "abc", + multiline3: "abc", +}); +`, + }); + }); + + it("hug array literal", async () => { + await assertFormat({ + code: ` +const foo = utcDateTime. fromFoo(#[ + "very very long array", + "very very long array", + "very very long array" +]); +`, + expected: ` +const foo = utcDateTime.fromFoo(#[ + "very very long array", + "very very long array", + "very very long array" +]); +`, + }); + }); + }); + describe("comments", () => { it("format comment at position 0", async () => { await assertFormat({ @@ -1807,7 +1860,7 @@ namespace Foo { }); }); - describe("string literals", () => { + describe("single line string literals", () => { it("format single line string literal", async () => { await assertFormat({ code: ` @@ -1835,28 +1888,70 @@ model Foo {} `, }); }); + }); - it("format multi line string literal", async () => { + describe("multi line string literals", () => { + it("keeps trailing whitespaces", async () => { await assertFormat({ code: ` @doc( """ - -this is a doc. - that - span - multiple lines. +3 whitespaces + +and blank line above """ ) model Foo {} `, expected: ` @doc(""" + 3 whitespaces -this is a doc. - that + and blank line above + """) +model Foo {} +`, + }); + }); + + it("keeps indent relative to closing quotes", async () => { + await assertFormat({ + code: ` +@doc( """ +this is a doc. + that span multiple lines. -""") +""" + ) +model Foo {} +`, + expected: ` +@doc(""" + this is a doc. + that + span + multiple lines. + """) +model Foo {} +`, + }); + }); + + it("keeps escaped charaters", async () => { + await assertFormat({ + code: ` +@doc( """ +with \\n +and \\t +""" + ) +model Foo {} +`, + expected: ` +@doc(""" + with \\n + and \\t + """) model Foo {} `, }); @@ -2847,11 +2942,11 @@ alias T = "foo \${{ await assertFormat({ code: ` alias T = """ - This \${ "one" } goes over - multiple - \${ "two" } - lines - """;`, + This \${ "one" } goes over + multiple + \${ "two" } + lines + """;`, expected: ` alias T = """ This \${"one"} goes over diff --git a/packages/compiler/test/mutators.test.ts b/packages/compiler/test/mutators.test.ts new file mode 100644 index 00000000000..872a1e87133 --- /dev/null +++ b/packages/compiler/test/mutators.test.ts @@ -0,0 +1,35 @@ +import { beforeEach, describe, it } from "vitest"; +import { Mutators, mutateSubgraph } from "../src/core/mutator.js"; +import { Model } from "../src/index.js"; +import { createTestHost } from "../src/testing/test-host.js"; +import { createTestWrapper } from "../src/testing/test-utils.js"; +import { BasicTestRunner, TestHost } from "../src/testing/types.js"; + +describe("compiler: Mutators", () => { + let host: TestHost; + let runner: BasicTestRunner; + + beforeEach(async () => { + host = await createTestHost(); + runner = createTestWrapper(host); + }); + + describe("Visibility", () => { + it("works", async () => { + const code = ` + @test model Foo { + @visibility("create") x: string; + y: string; + }; + `; + + const { Foo } = (await runner.compile(code)) as { Foo: Model }; + const mutated = mutateSubgraph( + runner.program, + [Mutators.Visibility.update, Mutators.JSONMergePatch], + Foo + ); + console.log([...(mutated.type as Model).properties]); + }); + }); +}); diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index ec3074d363b..e70bd01501e 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -1007,6 +1007,19 @@ describe("compiler: parser", () => { strictEqual(docs[0].tags.length, 0); }, ], + [ + ` + /** Escape at the end \\*/ + model M {} + `, + (script) => { + const docs = script.statements[0].docs; + strictEqual(docs?.length, 1); + strictEqual(docs[0].content.length, 1); + strictEqual(docs[0].content[0].text, "Escape at the end \\"); + strictEqual(docs[0].tags.length, 0); + }, + ], [ ` /** @@ -1024,6 +1037,8 @@ describe("compiler: parser", () => { *\`\`\` * * \`This is not a @tag either because we're in a code span\`. + * + * This is not a \\@tag because it is escaped. * * @param x the param * that continues on another line @@ -1052,7 +1067,9 @@ describe("compiler: parser", () => { "This code fence is glued\n" + "to the stars\n" + "```\n\n" + - "`This is not a @tag either because we're in a code span`." + "`This is not a @tag either because we're in a code span`.\n" + + "\n" + + "This is not a @tag because it is escaped." ); strictEqual(docs[0].tags.length, 6); const [xParam, yParam, tTemplate, uTemplate, returns, pretend] = docs[0].tags; diff --git a/packages/compiler/test/realm.test.ts b/packages/compiler/test/realm.test.ts new file mode 100644 index 00000000000..4e3c2350bf7 --- /dev/null +++ b/packages/compiler/test/realm.test.ts @@ -0,0 +1,211 @@ +/* +interface StateContext { + program: Program; + realm?: Realm; +} +function createColorDecorators(host: TestHost, runner: BasicTestRunner) { + const libDef = createTypeSpecLibrary({ + name: "colors", + diagnostics: {}, + }); + + const blueKey = libDef.createStateSymbol("colors"); + + const defs = { + isBlue(context: StateContext, t: Type): boolean { + return !!state(context, blueKey).get(t); + }, + $blue(context: DecoratorContext, target: Type) { + state(context, blueKey).set(target, true); + }, + context: createContext, + blueKey, + }; + + host.addJsFile("colors.js", defs); + + return defs; + + function createContext(realm?: Realm): StateContext { + return { + program: runner.program, + realm, + }; + } +} + +function cloneIntoRealm(runner: BasicTestRunner, target: T) { + const realm = new Realm(runner.program, "test realm"); + const clone = realm.clone(target) as T; + return { realm, clone }; +} + +describe("compiler: realm", () => { + let host: TestHost; + let runner: BasicTestRunner; + let decs: ReturnType; + + beforeEach(async () => { + host = await createTestHost(); + runner = createTestWrapper(host, { + autoImports: ["./colors.js"], + }); + decs = createColorDecorators(host, runner); + }); + + describe("compiler: realm: clone", () => { + it("clones models", async () => { + const code = ` + @test model Bar { } + @test model Foo { x: Bar }; + `; + + const { Foo, Bar } = (await runner.compile(code)) as { Foo: Model; Bar: Model }; + const { realm, clone: shadowFoo } = cloneIntoRealm(runner, Foo); + + assert.notStrictEqual(Foo, shadowFoo, "creates a clone of models"); + assert(shadowFoo.properties.has("x"), "the clone has the right members"); + assert.strictEqual( + Foo.properties.get("x"), + shadowFoo.properties.get("x"), + "doesn't clone model properties" + ); + assert.strictEqual(shadowFoo.properties.get("x")!.type, Bar, "doesn't clone member types"); + + assert(realm.hasType(shadowFoo), "shadowFoo is in the realm"); + assert( + !realm.hasType(shadowFoo.properties.get("x")!), + "shadowFoo's x prop is not in the realm" + ); + assert(!realm.hasType(Foo), "Foo is not in the realm"); + assert(!realm.hasType(Bar), "Bar is not in the realm"); + }); + + it("handles cloned state", async () => { + const code = ` + @blue @test model Bar { } + @blue @test model Foo { x: Bar }; + `; + + const { Foo, Bar } = (await runner.compile(code)) as { Foo: Model; Bar: Model }; + const { realm, clone: shadowFoo } = cloneIntoRealm(runner, Foo); + const context = decs.context(realm); + assert(Foo !== shadowFoo, "shadowFoo is actually a clone"); + assert(!decs.isBlue(context, Foo), "Foo is not blue"); + assert(decs.isBlue(context, Bar), "Bar is blue"); + assert(decs.isBlue(context, shadowFoo), "shadowFoo is blue"); + + const realmItems = new Set([...state(context, decs.blueKey).keys()]); + assert(!realmItems.has(Foo), "realm state does not have Foo"); + assert(realmItems.has(Bar), "realm state has Bar"); + assert(realmItems.has(shadowFoo), "realm state has shadowFoo"); + + const mainItems = new Set([...state({ ...context, realm: undefined }, decs.blueKey).keys()]); + assert(mainItems.has(Bar), "main state has Bar"); + assert(mainItems.has(Foo), "main state has Foo"); + assert(!mainItems.has(shadowFoo), "main state does not have shadowFoo"); + }); + }); +}); + +describe("compiler: VisibilityRealm", () => { + let host: TestHost; + let runner: BasicTestRunner; + + class VisibilityRealm extends Realm { + constructor(program: Program, visibility: string) { + super(program, `Visibility[${visibility}]`, [Mutators.Visibility.read]); + } + } + + beforeEach(async () => { + host = await createTestHost(); + runner = createTestWrapper(host); + }); + + it("recursively clones into the realm, removing non-visible properties", async () => { + const code = ` + @test model Foo { + @visibility("create") x: string; + @visibility("read") y: Bar; + }; + + @test model Bar { + @visibility("create") x: string; + @visibility("read") y: string; + } + `; + + const { Foo, Bar } = (await runner.compile(code)) as { Foo: Model; Bar: Model }; + const realm = new VisibilityRealm(runner.program, "read"); + const shadowFoo = realm.clone(Foo); + const shadowBar = realm.map(Bar)!; + + assert.strictEqual(shadowFoo.properties.size, 1); + assert(shadowFoo.properties.has("y")); + assert(!shadowFoo.properties.has("x")); + assert(realm.updatesType(Foo.properties.get("x")!)); + assert(!realm.updatesType(Foo.properties.get("y")!)); + + assert.strictEqual(shadowBar.properties.size, 1); + assert(shadowBar.properties.has("y")); + assert(!shadowBar.properties.has("x")); + assert(realm.updatesType(Bar.properties.get("x")!)); + assert(!realm.updatesType(Bar.properties.get("y")!)); + }); +}); + +describe("Compiler: update and JSON Merge Patch realm", () => { + let host: TestHost; + let runner: BasicTestRunner; + + beforeEach(async () => { + host = await createTestHost(); + runner = createTestWrapper(host); + }); + + it("recursively clones into the realm, removing non-visible properties", async () => { + const code = ` + @test model Foo { + @visibility("create") x: string; + y: Bar; + z?: string; + }; + + @test model Bar { + @visibility("create") deleted: string; + x: string; + y?: string; + } + `; + + const { Foo, Bar } = (await runner.compile(code)) as { Foo: Model; Bar: Model }; + const realm = new Realm(runner.program, "update and merge", [ + Mutators.Visibility.update, + Mutators.JSONMergePatch, + ]); + const shadowFoo = realm.clone(Foo); + const shadowBar = realm.map(Bar)!; + + assert(shadowFoo.name === "FooUpdateMergePatch", "shadowFoo name"); + assert(!shadowFoo.properties.has("x"), "shadowFoo doesn't have x"); + assert(shadowFoo.properties.get("y")!.optional, "shadowFoo y is optional"); + assert(shadowFoo.properties.get("y")!.type === Bar, "shadowFoo's y type is Bar"); + assert(shadowFoo.properties.get("z")!.optional, "shadowFoo z is optional"); + assert(shadowFoo.properties.get("z")!.type.kind === "Union", "shadowFoo z is a union"); + const zVariants = [...(shadowFoo.properties.get("z")!.type as Union).variants.values()]; + assert( + zVariants[0].type === runner.program.checker.getStdType("string"), + "z's union has string" + ); + assert(zVariants[1].type === runner.program.checker.nullType, "z's union has null"); + + assert(shadowBar.name === "BarUpdateMergePatch", "shadowBar name"); + assert(shadowBar.properties.get("x")!.optional, "shadowBar x is optional"); + assert( + shadowBar.properties.get("x")!.type === runner.program.checker.getStdType("string"), + "shadowBar type is string" + ); + }); +}); +*/ diff --git a/packages/compiler/test/server/colorization.test.ts b/packages/compiler/test/server/colorization.test.ts index 05a1173b7be..c85a170f51f 100644 --- a/packages/compiler/test/server/colorization.test.ts +++ b/packages/compiler/test/server/colorization.test.ts @@ -1443,6 +1443,23 @@ function testColorization(description: string, tokenize: Tokenize) { ]); }); + it("tokenize @prop", async () => { + const tokens = await tokenizeDocComment( + `/** + * Doc comment + * @prop foo Foo desc + */ + alias A = 1;` + ); + + deepStrictEqual(tokens, [ + Token.tspdoc.tag("@"), + Token.tspdoc.tag("prop"), + Token.identifiers.variable("foo"), + ...common, + ]); + }); + it("tokenize @returns", async () => { const tokens = await tokenizeDocComment( `/** diff --git a/packages/compiler/test/server/completion.test.ts b/packages/compiler/test/server/completion.test.ts index 8dd65e87159..093839c2a05 100644 --- a/packages/compiler/test/server/completion.test.ts +++ b/packages/compiler/test/server/completion.test.ts @@ -1,4 +1,4 @@ -import { deepStrictEqual, ok, strictEqual } from "assert"; +import { deepStrictEqual, equal, ok, strictEqual } from "assert"; import { describe, it } from "vitest"; import { CompletionItem, @@ -59,6 +59,119 @@ describe("complete statement keywords", () => { }); }); +describe("completes for keywords", () => { + describe.each([ + [`scalar S ┆`, ["extends"]], + [`scalar S ┆ `, ["extends"]], + [`scalar S \n┆\n`, ["extends"]], + [`scalar S ┆;`, ["extends"]], + [`scalar S ┆ ;`, ["extends"]], + [`scalar S /*comment*/ ┆{}`, ["extends"]], + [`scalar S ┆ {}`, ["extends"]], + [`scalar S ┆ \nscalar S2`, ["extends"]], + [`scalar S1;\nscalar S2 ┆ S1`, ["extends"]], + [`scalar S1;\nscalar S2 e┆x S1`, ["extends"]], + [`scalar S1;\nscalar S2 ┆ex S1`, ["extends"]], + [`scalar S ┆\n`, ["extends"]], + [`scalar S┆ \n`, ["extends"]], + [`scalar S ┆ {}`, ["extends"]], + [`scalar S ex┆`, ["extends"]], + [`scalar S ex┆tends`, ["extends"]], + [`scalar S ex ┆ {}`, []], + [`scalar S ex ex┆`, []], + [`scalar S {┆}`, ["init"]], + [`scalar S`, []], + + [`model M ┆`, ["extends", "is"]], + [`model M ┆ `, ["extends", "is"]], + [`model M \n┆\n`, ["extends", "is"]], + [`model M ┆;`, ["extends", "is"]], + [`model M ┆ ;`, ["extends", "is"]], + [`model M ┆{}`, ["extends", "is"]], + [`model M ┆ {}`, ["extends", "is"]], + [`model M ┆ \nscalar S2`, ["extends", "is"]], + [`model M1{}; model M2 ┆ M1`, ["extends", "is"]], + [`model M1{}; model M2 e┆x M1`, ["extends", "is"]], + [`model M1{}; model M2 ┆ex M1`, ["extends", "is"]], + [`model M ┆\n`, ["extends", "is"]], + [`model M┆ \n`, ["extends", "is"]], + [`model M ┆ {}`, ["extends", "is"]], + [`model M ex┆`, ["extends", "is"]], + [`model M i┆s`, ["extends", "is"]], + [`model M {┆}`, []], + [`model M {}`, []], + + [`op o ┆`, ["is"]], + [`op o ┆ `, ["is"]], + [`op o \n┆\n`, ["is"]], + [`op o ┆;`, ["is"]], + [`op o ┆ ;`, ["is"]], + [`op o ┆{}`, ["is"]], + [`op o ┆ {}`, ["is"]], + [`op o ┆ ()`, ["is"]], + [`op o ┆()`, ["is"]], + [`op o ┆ \nscalar S2`, ["is"]], + [`op o1{}; op o2 \n//comment\n ┆ M1`, ["is"]], + [`op o1{}; op o2 i┆s M1`, ["is"]], + [`op o1{}; op o2 ┆is M1`, ["is"]], + [`op o ┆\n`, ["is"]], + [`op o┆ \n`, ["is"]], + [`op o ┆ {}`, ["is"]], + [`op o is┆`, ["is"]], + [`op o (┆)`, []], + [`op o {}`, []], + [`interface I {o ┆}`, ["is"]], + [`interface I {o ┆ ()}`, ["is"]], + [`interface I {o (┆)}`, []], + + [`interface I ┆`, ["extends"]], + [`interface I //comment\n ┆ `, ["extends"]], + [`interface I \n┆\n`, ["extends"]], + [`interface I ┆;`, ["extends"]], + [`interface I ┆ ;`, ["extends"]], + [`interface I ┆{}`, ["extends"]], + [`interface I ┆ {}`, ["extends"]], + [`interface I ┆ \nscalar S2`, ["extends"]], + [`interface I1;\ninterface I2 ┆ I1`, ["extends"]], + [`interface I1;\ninterface I2 e┆x I1`, ["extends"]], + [`interface I1;\ninterface I2 ┆ex I1`, ["extends"]], + [`interface I ┆\n`, ["extends"]], + [`interface I┆ \n`, ["extends"]], + [`interface I ┆ {}`, ["extends"]], + [`interface I ex┆`, ["extends"]], + [`interface I ex┆tends`, ["extends"]], + [`interface I ex ┆ {}`, []], + [`interface I ex ex┆`, []], + [`interface I {┆}`, []], + [`interface I`, []], + + [`scalar S`, ["extends"]], + [`scalar S`, ["extends"]], + [`model M`, ["extends"]], + [`model M`, ["extends"]], + [`model M`, ["extends"]], + [`op o`, ["extends"]], + [`op o`, ["extends"]], + [`interface I┆`, []], + [`interface I<┆, T, Q>`, []], + [`interface I`, ["extends"]], + [`model M{};alias a = M`, []], + [`model M{};model M2 extends M`, []], + ] as const)("%s", (code, keywords) => { + it("completes extends keyword", async () => { + const completions = await complete(code); + if (keywords.length > 0) { + check( + completions, + keywords.map((w) => ({ label: w, kind: CompletionItemKind.Keyword })) + ); + } else { + equal(completions.items.length, 0, "No completions expected"); + } + }); + }); +}); + describe("imports", () => { describe("library imports", () => { async function testCompleteLibrary(code: string) { diff --git a/packages/compiler/test/server/misc.test.ts b/packages/compiler/test/server/misc.test.ts index 8ff5218b52e..176d8f7137c 100644 --- a/packages/compiler/test/server/misc.test.ts +++ b/packages/compiler/test/server/misc.test.ts @@ -1,6 +1,6 @@ import { ok, strictEqual } from "assert"; import { describe, it } from "vitest"; -import { Node, SyntaxKind, TypeSpecScriptNode, parse } from "../../src/core/index.js"; +import { PositionDetail, SyntaxKind, TypeSpecScriptNode, parse } from "../../src/core/index.js"; import { getCompletionNodeAtPosition } from "../../src/server/serverlib.js"; import { extractCursor } from "../../src/testing/test-server-host.js"; import { dumpAST } from "../ast-test-utils.js"; @@ -9,57 +9,62 @@ describe("compiler: server: misc", () => { describe("getCompletionNodeAtPosition", () => { async function getNodeAtCursor( sourceWithCursor: string - ): Promise<{ root: TypeSpecScriptNode; node: Node | undefined }> { + ): Promise<{ root: TypeSpecScriptNode; detail: PositionDetail | undefined }> { const { source, pos } = extractCursor(sourceWithCursor); - const root = parse(source); + const root = parse(source, { comments: true, docs: true }); dumpAST(root); - return { node: getCompletionNodeAtPosition(root, pos)?.node, root }; + return { detail: getCompletionNodeAtPosition(root, pos), root }; } it("return identifier for property return type", async () => { - const { node } = await getNodeAtCursor(` + const { detail } = await getNodeAtCursor(` model Foo { prop: stri┆ng } `); + const node = detail?.node; ok(node); strictEqual(node.kind, SyntaxKind.Identifier as const); strictEqual(node.sv, "string"); }); it("return missing identifier node when at the position for model property type", async () => { - const { node } = await getNodeAtCursor(` + const { detail } = await getNodeAtCursor(` model Foo { prop: ┆ } `); + const node = detail?.getPositionDetailAfterTrivia()?.node; ok(node); strictEqual(node.kind, SyntaxKind.Identifier as const); strictEqual(node.sv, "1"); }); it("return string literal when in non completed string", async () => { - const { node } = await getNodeAtCursor(` + const { detail } = await getNodeAtCursor(` import "┆ `); + const node = detail?.node; ok(node); strictEqual(node.kind, SyntaxKind.StringLiteral); }); it("return string literal when in non completed multi line string", async () => { - const { node } = await getNodeAtCursor(` + const { detail } = await getNodeAtCursor(` model Foo { prop: """┆ } `); + const node = detail?.node; ok(node); strictEqual(node.kind, SyntaxKind.StringLiteral); }); it("return missing identifier between dot and close paren", async () => { - const { node } = await getNodeAtCursor(` + const { detail } = await getNodeAtCursor(` @myDecN.┆) `); + const node = detail?.node; ok(node); strictEqual(node.kind, SyntaxKind.Identifier as const); strictEqual(node.sv, "1"); @@ -67,11 +72,12 @@ describe("compiler: server: misc", () => { describe("resolve real node when no potential identifier", () => { it("return namespace when in namespace body", async () => { - const { node } = await getNodeAtCursor(` + const { detail } = await getNodeAtCursor(` namespace Foo { ┆ } `); + const node = detail?.node; ok(node); strictEqual(node.kind, SyntaxKind.NamespaceStatement as const); strictEqual(node.id.sv, "Foo"); diff --git a/packages/efnext-cli-sketch/generated-defs/TypeSpecCLI.ts b/packages/efnext-cli-sketch/generated-defs/TypeSpecCLI.ts new file mode 100644 index 00000000000..794e7281e36 --- /dev/null +++ b/packages/efnext-cli-sketch/generated-defs/TypeSpecCLI.ts @@ -0,0 +1,22 @@ +import type { + DecoratorContext, + Interface, + ModelProperty, + Namespace, + Operation, +} from "@typespec/compiler"; + +export type ShortDecorator = ( + context: DecoratorContext, + target: ModelProperty, + value: string +) => void; + +export type PositionalDecorator = (context: DecoratorContext, target: ModelProperty) => void; + +export type InvertableDecorator = (context: DecoratorContext, target: ModelProperty) => void; + +export type CliDecorator = ( + context: DecoratorContext, + target: Namespace | Interface | Operation +) => void; diff --git a/packages/efnext-cli-sketch/generated-defs/TypeSpecCLI.ts-test.ts b/packages/efnext-cli-sketch/generated-defs/TypeSpecCLI.ts-test.ts new file mode 100644 index 00000000000..cca11db21c1 --- /dev/null +++ b/packages/efnext-cli-sketch/generated-defs/TypeSpecCLI.ts-test.ts @@ -0,0 +1,23 @@ +/** An error here would mean that the decorator is not exported or doesn't have the right name. */ +import { $cli, $invertable, $positional, $short } from "@typespec/efnext-cli-sketch"; +import type { + CliDecorator, + InvertableDecorator, + PositionalDecorator, + ShortDecorator, +} from "./TypeSpecCLI.js"; + +type Decorators = { + $short: ShortDecorator; + $positional: PositionalDecorator; + $invertable: InvertableDecorator; + $cli: CliDecorator; +}; + +/** An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ +const _: Decorators = { + $short, + $positional, + $invertable, + $cli, +}; diff --git a/packages/efnext-cli-sketch/lib/main.tsp b/packages/efnext-cli-sketch/lib/main.tsp new file mode 100644 index 00000000000..d0dcf7737d3 --- /dev/null +++ b/packages/efnext-cli-sketch/lib/main.tsp @@ -0,0 +1,8 @@ +import "../dist/src/decorators.js"; + +namespace TypeSpecCLI; + +extern dec short(target: Reflection.ModelProperty, value: valueof string); +extern dec positional(target: Reflection.ModelProperty); +extern dec invertable(target: Reflection.ModelProperty); +extern dec cli(target: Reflection.Namespace | Reflection.Interface | Reflection.Operation); diff --git a/packages/efnext-cli-sketch/package.json b/packages/efnext-cli-sketch/package.json new file mode 100644 index 00000000000..672fb31af8b --- /dev/null +++ b/packages/efnext-cli-sketch/package.json @@ -0,0 +1,79 @@ +{ + "name": "@typespec/efnext-cli-sketch", + "version": "0.56.0", + "author": "Microsoft Corporation", + "description": "emitter framework prototype", + "homepage": "https://github.com/microsoft/typespec", + "readme": "https://github.com/microsoft/typespec/blob/main/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", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "default": "./dist/src/index.js" + } + }, + "tspMain": "lib/main.tsp", + "engines": { + "node": ">=18.0.0" + }, + "scripts": { + "clean": "rimraf ./dist ./temp", + "build": "npm run gen-extern-signature && tsc -p . && npm run lint-typespec-library", + "watch": "tsc -p . --watch", + "gen-extern-signature": "tspd --enable-experimental gen-extern-signature .", + "lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit", + "test": "vitest run", + "test:ui": "vitest --ui", + "test:ci": "vitest run --coverage --reporter=junit --reporter=default", + "lint": "eslint . --max-warnings=0", + "lint:fix": "eslint . --fix", + "regen-docs": "tspd doc . --enable-experimental --output-dir ../../docs/emitters/json-schema/reference" + }, + "files": [ + "lib/*.tsp", + "dist/**", + "!dist/test/**" + ], + "peerDependencies": { + "@typespec/compiler": "workspace:~", + "@typespec/http": "workspace:~" + }, + "devDependencies": { + "@types/marked": "^6.0.0", + "@types/marked-terminal": "^6.1.1", + "@types/node": "~18.11.19", + "@typespec/compiler": "workspace:~", + "@typespec/internal-build-utils": "workspace:~", + "@typespec/library-linter": "workspace:~", + "@typespec/tspd": "workspace:~", + "@vitest/coverage-v8": "^1.6.0", + "@vitest/ui": "^1.6.0", + "ajv": "~8.13.0", + "ajv-formats": "~3.0.1", + "c8": "^9.1.0", + "rimraf": "~5.0.7", + "typescript": "~5.4.5", + "vitest": "^1.6.0" + }, + "dependencies": { + "@typespec/efnext": "workspace:~", + "change-case": "~5.4.4", + "marked": "^13.0.0", + "marked-terminal": "^7.1.0", + "prettier": "~3.2.5", + "strip-ansi": "^7.1.0", + "yaml": "~2.4.2" + } +} diff --git a/packages/efnext-cli-sketch/sample/main.tsp b/packages/efnext-cli-sketch/sample/main.tsp new file mode 100644 index 00000000000..f6bc4978247 --- /dev/null +++ b/packages/efnext-cli-sketch/sample/main.tsp @@ -0,0 +1,28 @@ +import "@typespec/efnext-cli-sketch"; +using TypeSpecCLI; +/** + * This is a **really** awesome Todo CLI tool. It makes + * managing your todo items very easy. + * + * # Usage + * + * ```bash + * todo-cli create --title "title" --description "desc" + * todo-cli finish 42 + * ``` + */ +@cli +interface TodoCLI { + /** Create a todo item. */ + create(todoItem: TodoItem, @invertable color?: boolean): void; + /** Finish a todo item. */ + finish(@positional id: int32): void; +} + +model TodoItem { + /** The todo item's title. */ + @short("t") title: string; + + /** The todo item's description. */ + @short("d") description: string; +} diff --git a/packages/efnext-cli-sketch/sample/tspconfig.yaml b/packages/efnext-cli-sketch/sample/tspconfig.yaml new file mode 100644 index 00000000000..88087ce7a7e --- /dev/null +++ b/packages/efnext-cli-sketch/sample/tspconfig.yaml @@ -0,0 +1,2 @@ +emit: + - "@typespec/efnext-cli-sketch" diff --git a/packages/efnext-cli-sketch/src/components/CommandArgParser/CommandArgParser.tsx b/packages/efnext-cli-sketch/src/components/CommandArgParser/CommandArgParser.tsx new file mode 100644 index 00000000000..e7ca35f4da1 --- /dev/null +++ b/packages/efnext-cli-sketch/src/components/CommandArgParser/CommandArgParser.tsx @@ -0,0 +1,50 @@ +/* eslint-disable unicorn/filename-case */ +import { Interface, ModelProperty, Namespace, Operation } from "@typespec/compiler"; +import { createContext, useContext } from "@typespec/efnext/framework"; +import { FunctionDeclaration } from "@typespec/efnext/typescript"; +import { CliType } from "../../index.js"; +import { HelpText } from "../HelpText.js"; +import { GetTokens } from "./GetTokens.js"; +import { MarshalledArgsInit } from "./MarshalledArgsInit.js"; +import { TokenLoop } from "./TokenLoop.js"; + +interface CommandContext { + command: CliType; + options: Map; + subcommandMap: Map; +} + +const CommandContext = createContext(); + +export function useCommand() { + return useContext(CommandContext)!; +} + +export interface CommandArgParserProps { + command: Operation | Namespace | Interface; + options: Map; +} + +export function CommandArgParser({ command, options }: CommandArgParserProps) { + const hasSubcommands = command.kind === "Namespace" || command.kind === "Interface"; + const subcommands = hasSubcommands + ? [...(command as Namespace | Interface).operations.values()] + : []; + + // map of subcommand name to the operation for that subcommand + const subcommandMap = new Map(); + for (const subcommand of subcommands) { + subcommandMap.set(subcommand.name, subcommand); + } + + return ( + + + + + + + + + ); +} diff --git a/packages/efnext-cli-sketch/src/components/CommandArgParser/GetTokens.tsx b/packages/efnext-cli-sketch/src/components/CommandArgParser/GetTokens.tsx new file mode 100644 index 00000000000..0122b3fa577 --- /dev/null +++ b/packages/efnext-cli-sketch/src/components/CommandArgParser/GetTokens.tsx @@ -0,0 +1,40 @@ +/* eslint-disable unicorn/filename-case */ +import { code } from "@typespec/efnext/framework"; +import { $verbatim, ObjectValue } from "@typespec/efnext/typescript"; +import { useHelpers } from "../../helpers.js"; +import { useCommand } from "./CommandArgParser.js"; + +export interface GetTokensProps {} + +// eslint-disable-next-line no-empty-pattern +export function GetTokens({}: GetTokensProps) { + const { options } = useCommand(); + const helpers = useHelpers(); + + const parseArgsArg: Record = { + args: $verbatim("args"), + tokens: true, + strict: false, + options: {}, + }; + + // assemble the options in parseArgsArg and arg handlers. + for (const [option, path] of options) { + const argOptions: Record = {}; + parseArgsArg.options[option.name] = argOptions; + + if (helpers.boolean.is(option.type)) { + argOptions.type = "boolean"; + } else { + argOptions.type = "string"; + } + + if (helpers.option.hasShortName(option)) { + argOptions.short = helpers.option.getShortName(option); + } + } + + return code` + const { tokens } = nodeParseArgs(${()}); + `; +} diff --git a/packages/efnext-cli-sketch/src/components/CommandArgParser/MarshalledArgsInit.tsx b/packages/efnext-cli-sketch/src/components/CommandArgParser/MarshalledArgsInit.tsx new file mode 100644 index 00000000000..5b7fe26f6ca --- /dev/null +++ b/packages/efnext-cli-sketch/src/components/CommandArgParser/MarshalledArgsInit.tsx @@ -0,0 +1,53 @@ +/* eslint-disable unicorn/filename-case */ +import { Model, ModelProperty } from "@typespec/compiler"; +import { code } from "@typespec/efnext/framework"; +import { Value } from "@typespec/efnext/typescript"; +import { useCommand } from "./CommandArgParser.js"; + +export interface MarshalledArgsInit {} + +// eslint-disable-next-line no-empty-pattern +export function MarshalledArgsInit({}: MarshalledArgsInit) { + const { command } = useCommand(); + let defaultArgParams: ModelProperty[]; + if (command.kind === "Interface" || command.kind === "Namespace") { + // todo: get "command" op for this command group. + defaultArgParams = []; + } else { + defaultArgParams = [...command.parameters.properties.values()]; + } + const defaultArgs = defaultArgParams.map((p) => buildDefaults(p)); + + return code` + const marshalledArgs: any[] = ${()}; + `; +} + +function buildDefaults(type: Model | ModelProperty) { + if (type.kind === "ModelProperty") { + if (type.defaultValue) { + switch (type.defaultValue.valueKind) { + case "BooleanValue": + case "StringValue": + case "NullValue": + return type.defaultValue.value; + case "NumericValue": + return type.defaultValue.value.asNumber(); + default: + throw new Error( + "default value kind of " + type.defaultValue.valueKind + " not supported" + ); + } + } else if (type.type.kind === "Model") { + return buildDefaults(type.type); + } else { + return undefined; + } + } else { + const defaultValue: Record = {}; + for (const prop of type.properties.values()) { + defaultValue[prop.name] = buildDefaults(prop); + } + return defaultValue; + } +} diff --git a/packages/efnext-cli-sketch/src/components/CommandArgParser/OptionTokenHandler.tsx b/packages/efnext-cli-sketch/src/components/CommandArgParser/OptionTokenHandler.tsx new file mode 100644 index 00000000000..d682d85a88c --- /dev/null +++ b/packages/efnext-cli-sketch/src/components/CommandArgParser/OptionTokenHandler.tsx @@ -0,0 +1,37 @@ +/* eslint-disable unicorn/filename-case */ +import { ModelProperty } from "@typespec/compiler"; +import { code } from "@typespec/efnext/framework"; +import { useHelpers } from "../../helpers.js"; + +export interface OptionTokenHandlerProps { + option: ModelProperty; + path: string; +} + +export function OptionTokenHandler({ option, path }: OptionTokenHandlerProps) { + const helpers = useHelpers(); + + if (helpers.boolean.is(option.type)) { + const cases = [$case(`marshalledArgs${path} = true`)]; + if (helpers.option.isInvertable(option)) { + cases.push($case(`marshalledArgs${path} = false`, "no-" + option.name)); + } + + return cases; + } else { + // todo: marshalling etc. + return $case(`marshalledArgs${path} = token.value!`); + } + + function $case(handler: string, optionName?: string) { + const names = optionName + ? [optionName] + : helpers.option.hasShortName(option) + ? [helpers.option.getShortName(option), option.name] + : [option.name]; + + return code` + ${names.map((v) => `case "${v}": `).join("")}${handler}; break; + `; + } +} diff --git a/packages/efnext-cli-sketch/src/components/CommandArgParser/PositionalTokenHandler.tsx b/packages/efnext-cli-sketch/src/components/CommandArgParser/PositionalTokenHandler.tsx new file mode 100644 index 00000000000..aa38dadf47c --- /dev/null +++ b/packages/efnext-cli-sketch/src/components/CommandArgParser/PositionalTokenHandler.tsx @@ -0,0 +1,28 @@ +/* eslint-disable unicorn/filename-case */ +import { code } from "@typespec/efnext/framework"; +import { useCommand } from "./CommandArgParser.js"; + +export interface PositionalTokenHandlerProps {} + +// eslint-disable-next-line no-empty-pattern +export function PositionalTokenHandler({}: PositionalTokenHandlerProps) { + const { subcommandMap } = useCommand(); + // todo: positionals. + if (subcommandMap && subcommandMap.size > 0) { + const subcommandCases = [...subcommandMap.entries()].map(([name, cli]) => { + return code` + case "${name}": parse${name}Args(args.slice(token.index + 1)); return; + `; + }); + // TODO: Fix this any cast + return code` + switch (token.value) { + ${subcommandCases as any} + } + `; + } else { + return code` + throw new Error("Unknown positional argument"); + `; + } +} diff --git a/packages/efnext-cli-sketch/src/components/CommandArgParser/TokenLoop.tsx b/packages/efnext-cli-sketch/src/components/CommandArgParser/TokenLoop.tsx new file mode 100644 index 00000000000..6b381dc1e09 --- /dev/null +++ b/packages/efnext-cli-sketch/src/components/CommandArgParser/TokenLoop.tsx @@ -0,0 +1,31 @@ +/* eslint-disable unicorn/filename-case */ +import { code } from "@typespec/efnext/framework"; +import { useCommand } from "./CommandArgParser.js"; +import { OptionTokenHandler } from "./OptionTokenHandler.js"; +import { PositionalTokenHandler } from "./PositionalTokenHandler.js"; + +export interface TokenLoopProps {} +// eslint-disable-next-line no-empty-pattern +export function TokenLoop({}: TokenLoopProps) { + const { command, options } = useCommand(); + const optionTokenHandlers = Array.from(options.entries()).map(([option, path]) => ( + + )); + + return code` + for (const token of tokens) { + if (token.kind === "positional") { + ${()} + } else if (token.kind === "option") { + switch (token.name) { + case "h": + case "help": + ${command.name}Help(); + return; + ${optionTokenHandlers} + } + } + } + (handler.${command.name} as any)(... marshalledArgs); + `; +} diff --git a/packages/efnext-cli-sketch/src/components/ControllerInterface.tsx b/packages/efnext-cli-sketch/src/components/ControllerInterface.tsx new file mode 100644 index 00000000000..d7ce7574678 --- /dev/null +++ b/packages/efnext-cli-sketch/src/components/ControllerInterface.tsx @@ -0,0 +1,60 @@ +/* eslint-disable unicorn/filename-case */ +import { Operation, Type, navigateType } from "@typespec/compiler"; +import { isDeclaration } from "@typespec/efnext/framework"; +import { + InterfaceDeclaration, + InterfaceMember, + TypeDeclaration, +} from "@typespec/efnext/typescript"; +import { useHelpers } from "../helpers.js"; +import { CliType } from "../index.js"; + +export interface ControllerInterfaceProps { + cli: CliType; +} + +export function ControllerInterface({ cli }: ControllerInterfaceProps) { + const commands: Operation[] = []; + const helpers = useHelpers(); + if (cli.kind === "Interface" || cli.kind === "Namespace") { + // TODO: Namespaces might have operation templates, probably need to find those? + commands.push(...cli.operations.values()); + } else { + commands.push(cli); + } + + const typeDecls = collectTypeDecls(cli).map((type) => ); + + const memberDecls = commands.map((command) => { + const optionsBagForm = helpers.toOptionsBag(command); + return ; + }); + return ( + <> + + {`${cli.name}: () => void;`} + {memberDecls} + version: string; + + {typeDecls} + + ); +} + +// todo: make this better. +function collectTypeDecls(root: Type) { + const types: Type[] = []; + navigateType( + root, + { + model(m) { + if (isDeclaration(m)) { + types.push(m); + } + }, + }, + {} + ); + + return types; +} diff --git a/packages/efnext-cli-sketch/src/components/HelpText.tsx b/packages/efnext-cli-sketch/src/components/HelpText.tsx new file mode 100644 index 00000000000..95d6547246c --- /dev/null +++ b/packages/efnext-cli-sketch/src/components/HelpText.tsx @@ -0,0 +1,125 @@ +/* eslint-disable unicorn/filename-case */ +import { ModelProperty } from "@typespec/compiler"; +import { code } from "@typespec/compiler/emitter-framework"; +import { FunctionDeclaration } from "@typespec/efnext/typescript"; +import { marked } from "marked"; +import { markedTerminal } from "marked-terminal"; +import pc from "picocolors"; +import stripAnsi from "strip-ansi"; +import { useHelpers } from "../helpers.js"; +import { CliType } from "../index.js"; +import { useCommand } from "./CommandArgParser/CommandArgParser.js"; + +function removeHashAndBold(s: string) { + return pc.bold(s.replace(/^#+ /, "")); +} + +marked.use( + markedTerminal({ + paragraph: (s: string) => { + return s.replace(/\n/g, " "); + }, + firstHeading: removeHashAndBold, + heading: removeHashAndBold, + }) as any +); +marked.use({ + breaks: false, +}); + +export interface HelpTextProps {} + +// TODO: Accumulate output in an array, join, and write with process.stdout.write. +// output code should be a clsoe to a single process.stdout.write with a string. +// although the tables will make that impossible to do entirely. + +// eslint-disable-next-line no-empty-pattern +export function HelpText({}: HelpTextProps) { + const { command, options, subcommandMap } = useCommand(); + const helpers = useHelpers(); + const commandDoc = helpers.getDoc(command); + const commandDesc = commandDoc + ? ((marked(commandDoc) as string).trimEnd() + "\n").replace(/\n/g, "\\n").replace(/"/g, '\\"') + : ""; + const helpTable = [...options.keys()] + .sort((a, b) => (a.name > b.name ? 1 : b.name > a.name ? -1 : 0)) + .map((o) => pushOptionHelp(o)) + .join(""); + + let subcommandHelp = ""; + if (subcommandMap.size > 0) { + subcommandHelp += ` + const subcommandTable = new Table({ + chars: noFormatting, + }); + `; + subcommandHelp += [...subcommandMap.entries()] + .map(([name, cli]) => { + return pushSubcommandHelp(name, cli); + }) + .join(""); + + subcommandHelp += ` + console.log(\`\\n${pc.bold("Subcommands\n")}\`); + console.log(subcommandTable.toString()); + `; + } + return ( + + {code` + if (noColor || process.env["NO_COLOR"]) { + console.log("${command.name} " + handler.version + "\\n"); + console.log("${stripAnsi(commandDesc)}"); + } else { + console.log("${command.name} \" + handler.version + \"\\n"); + console.log("${commandDesc}"); + } + + const noFormatting = { + top: "", + "top-mid": "", + "top-left": "", + "top-right": "", + "mid-mid": "", + mid: "", + middle: "", + bottom: "", + "bottom-mid": "", + "bottom-left": "", + "bottom-right": "", + left: "", + "left-mid": "", + right: "", + "right-mid": "", + }; + + const table = new Table({ + chars: noFormatting, + }); + table.push(["--help, -h", "Display this help message."]) + ${helpTable} + console.log(\`${pc.bold("Options\n")}\`); + console.log(table.toString()); + ${subcommandHelp} + `} + + ); + + function pushOptionHelp(option: ModelProperty) { + let options = `--${option.name}`; + + if (helpers.option.isInvertable(option)) { + options += `, --no-${option.name}`; + } + + if (helpers.option.hasShortName(option)) { + options += `, -${helpers.option.getShortName(option)}`; + } + + return `table.push([\`${options}\`, \`${helpers.getDoc(option)}\`]);`; + } + + function pushSubcommandHelp(name: string, cli: CliType) { + return `subcommandTable.push(["${name}", \`${helpers.getDoc(cli)}\`]);`; + } +} diff --git a/packages/efnext-cli-sketch/src/decorators.ts b/packages/efnext-cli-sketch/src/decorators.ts new file mode 100644 index 00000000000..489bfd71d96 --- /dev/null +++ b/packages/efnext-cli-sketch/src/decorators.ts @@ -0,0 +1,43 @@ +import type { Program, Type } from "@typespec/compiler"; + +import { stateKeys } from "./lib.js"; + +function stateFns(key: symbol) { + return { + has(context: { program: Program }, type: Type) { + return context.program.stateMap(key).has(type); + }, + get(context: { program: Program }, type: Type) { + return context.program.stateMap(key).get(type) as T; + }, + set(context: { program: Program }, type: Type, value?: T) { + context.program.stateMap(key).set(type, value ?? true); + }, + add(context: { program: Program }, type: Type) { + context.program.stateMap(key).set(type, true); + }, + keys(context: { program: Program }) { + return [...context.program.stateMap(key).keys()]; + }, + }; +} + +const shortStateFns = stateFns(stateKeys.short); +export const $short = shortStateFns.set; +export const hasShortName = shortStateFns.has; +export const getShortName = shortStateFns.get; + +const positionalStateFns = stateFns(stateKeys.positional); +export const isPositional = positionalStateFns.has; +export const $positional = positionalStateFns.set; + +const invertableStateFns = stateFns(stateKeys.invertable); +export const isInvertable = invertableStateFns.has; +export const $invertable = invertableStateFns.set; + +const cliStateFns = stateFns(stateKeys.cli); +export const isCli = cliStateFns.has; +export const listClis = cliStateFns.keys; +export const $cli = cliStateFns.add; + +export const namespace = "TypeSpecCLI"; diff --git a/packages/efnext-cli-sketch/src/emitter.tsx b/packages/efnext-cli-sketch/src/emitter.tsx new file mode 100644 index 00000000000..03419af76f3 --- /dev/null +++ b/packages/efnext-cli-sketch/src/emitter.tsx @@ -0,0 +1,102 @@ +import { + EmitContext, + Interface, + Model, + ModelProperty, + Namespace, + Operation, + Union, +} from "@typespec/compiler"; +import { EmitOutput, SourceFile, code, emit } from "@typespec/efnext/framework"; +import { CommandArgParser } from "./components/CommandArgParser/CommandArgParser.js"; +import { ControllerInterface } from "./components/ControllerInterface.js"; +import { HelperContext, getStateHelpers } from "./helpers.js"; + +export type CliType = Namespace | Interface | Operation; + +export async function $onEmit(context: EmitContext) { + console.time("emit"); + const helpers = getStateHelpers(context); + if (context.program.compilerOptions.noEmit) { + return; + } + + const clis = helpers.listClis() as CliType[]; + const cliSfs = []; + + for (let cli of clis) { + const subCommandClis = + cli.kind === "Namespace" || cli.kind === "Interface" ? [...cli.operations.values()] : []; + + const parsers = [cli, ...subCommandClis].map((cli) => { + const mutatedCli = + cli.kind === "Operation" ? (helpers.toOptionsBag(cli).type as Operation) : cli; + const options = collectCommandOptions(mutatedCli); + return ; + }); + + cliSfs.push( + + {code` + import { parseArgs as nodeParseArgs } from "node:util"; + import Table from "cli-table3"; + `} + + + {code` + export function parseArgs(args: string[], handler: CommandInterface) { + parse${cli.name}Args(args); + ${parsers} + }`} + + ); + } + + await emit( + context, + + {cliSfs} + + ); + console.timeEnd("emit"); +} + +export function collectCommandOptions(command: CliType): Map { + if (command.kind === "Namespace" || command.kind === "Interface") { + // TODO: find the root command operation + return new Map(); + } + const commandOpts = new Map(); + + const types: [Model | Union, string, boolean?][] = [[command.parameters, "", true]]; + + while (types.length > 0) { + const [type, path, topLevel] = types.pop()!; + + if (type.kind === "Model") { + let index = 0; + for (const param of type.properties.values()) { + const paramPath = topLevel ? `[${index}]` : `${path}.${param.name}`; + if (param.type.kind === "Model") { + types.push([param.type, paramPath]); + } else if ( + param.type.kind === "Union" && + [...param.type.variants.values()].find((v) => v.type.kind === "Model") + ) { + } else { + commandOpts.set(param, paramPath); + } + + index++; + } + } else if (type.kind === "Union") { + for (const variant of type.variants.values()) { + if (variant.type.kind === "Union" || variant.type.kind === "Model") { + types.push([variant.type, path]); + } + } + } + } + + return commandOpts; +} diff --git a/packages/efnext-cli-sketch/src/helpers.ts b/packages/efnext-cli-sketch/src/helpers.ts new file mode 100644 index 00000000000..0b6d72b5d91 --- /dev/null +++ b/packages/efnext-cli-sketch/src/helpers.ts @@ -0,0 +1,76 @@ +import { + EmitContext, + Mutator, + MutatorFlow, + Operation, + Type, + getDoc, + isIntrinsicType, + mutateSubgraph, +} from "@typespec/compiler"; +import { createContext, useContext } from "@typespec/efnext/framework"; +import { getShortName, hasShortName, isInvertable, isPositional, listClis } from "./decorators.js"; + +export const HelperContext = createContext>(); + +export function useHelpers() { + return useContext(HelperContext)!; +} + +export function getStateHelpers(context: EmitContext) { + return { + string: { + is(type: Type) { + if (type.kind !== "Scalar") return false; + return isIntrinsicType(context.program, type, "string"); + }, + }, + option: { + hasShortName: hasShortName.bind(undefined, context), + getShortName: getShortName.bind(undefined, context), + isPositional: isPositional.bind(undefined, context), + isInvertable: isInvertable.bind(undefined, context), + }, + boolean: { + is(type: Type) { + if (type.kind !== "Scalar") return false; + return isIntrinsicType(context.program, type, "boolean"); + }, + }, + getDoc: getDoc.bind(undefined, context.program), + listClis: listClis.bind(undefined, context), + toOptionsBag(type: Operation) { + return mutateSubgraph(context.program, [optionsBagMutator], type); + }, + }; +} + +const optionsBagMutator: Mutator = { + name: "Optionals in options bag", + Model: { + filter() { + return MutatorFlow.DontRecurse; + }, + mutate(m, clone, program, realm) { + const optionals = [...clone.properties.values()].filter((p) => p.optional); + + if (optionals.length === 0) { + return; + } + + const optionsBag = realm.typeFactory.model("", optionals); + const optionsProp = realm.typeFactory.modelProperty("options", optionsBag, { + optional: true, + }); + console.log(optionsProp); + + for (const [key, prop] of clone.properties) { + if (prop.optional) { + clone.properties.delete(key); + } + } + + clone.properties.set("options", optionsProp); + }, + }, +}; diff --git a/packages/efnext-cli-sketch/src/index.ts b/packages/efnext-cli-sketch/src/index.ts new file mode 100644 index 00000000000..ea2f2b1dc92 --- /dev/null +++ b/packages/efnext-cli-sketch/src/index.ts @@ -0,0 +1,2 @@ +export * from "./decorators.js"; +export * from "./emitter.js"; diff --git a/packages/efnext-cli-sketch/src/lib.ts b/packages/efnext-cli-sketch/src/lib.ts new file mode 100644 index 00000000000..9bf8f5aa0ca --- /dev/null +++ b/packages/efnext-cli-sketch/src/lib.ts @@ -0,0 +1,14 @@ +import { createTypeSpecLibrary } from "@typespec/compiler"; + +export const $lib = createTypeSpecLibrary({ + name: "typespec-cli", + diagnostics: {}, + state: { + cli: {}, + short: {}, + positional: {}, + invertable: {}, + }, +}); + +export const { reportDiagnostic, createDiagnostic, stateKeys } = $lib; diff --git a/packages/efnext-cli-sketch/src/testing/index.ts b/packages/efnext-cli-sketch/src/testing/index.ts new file mode 100644 index 00000000000..453a3317122 --- /dev/null +++ b/packages/efnext-cli-sketch/src/testing/index.ts @@ -0,0 +1,8 @@ +import { resolvePath } from "@typespec/compiler"; +import { createTestLibrary, TypeSpecTestLibrary } from "@typespec/compiler/testing"; +import { fileURLToPath } from "url"; + +export const TestLibrary: TypeSpecTestLibrary = createTestLibrary({ + name: "@typespec/efnext", + packageRoot: resolvePath(fileURLToPath(import.meta.url), "../../../"), +}); diff --git a/packages/efnext-cli-sketch/test/component-utils.tsx b/packages/efnext-cli-sketch/test/component-utils.tsx new file mode 100644 index 00000000000..ada0928d08a --- /dev/null +++ b/packages/efnext-cli-sketch/test/component-utils.tsx @@ -0,0 +1,31 @@ +import { EmitOutput, RenderedTreeNode, SourceFile, render } from "@typespec/efnext/framework"; +import { format } from "prettier"; +import { assert } from "vitest"; + +async function prepareExpected(expected: string) { + const expectedRoot = ( + + + {expected} + + + ); + + const rendered = await render(expectedRoot); + const raw = (rendered as any).flat(Infinity).join(""); + + return format(raw, { parser: "typescript" }); +} + +async function prepareActual(actual: RenderedTreeNode) { + const raw = (actual as any).flat(Infinity).join(""); + + return format(raw, { parser: "typescript" }); +} + +export async function assertEqual(actual: RenderedTreeNode, expected: string) { + const actualFormatted = await prepareActual(actual); + const expectedFormatted = await prepareExpected(expected); + + assert.equal(actualFormatted, expectedFormatted); +} diff --git a/packages/efnext-cli-sketch/test/test-host.ts b/packages/efnext-cli-sketch/test/test-host.ts new file mode 100644 index 00000000000..1c7641ab2d4 --- /dev/null +++ b/packages/efnext-cli-sketch/test/test-host.ts @@ -0,0 +1,69 @@ +import { Diagnostic, Program, resolvePath } from "@typespec/compiler"; +import { + createTestHost, + createTestWrapper, + expectDiagnosticEmpty, +} from "@typespec/compiler/testing"; +import { HttpTestLibrary } from "@typespec/http/testing"; +import { TestLibrary } from "../src/testing/index.js"; + +export async function createTypespecCliTestHost( + options: { libraries: "Http"[] } = { libraries: [] } +) { + const libraries = [TestLibrary]; + if (options.libraries.includes("Http")) { + libraries.push(HttpTestLibrary); + } + return createTestHost({ + libraries, + }); +} + +export async function createTypespecCliTestRunner() { + const host = await createTypespecCliTestHost(); + + return createTestWrapper(host, { + compilerOptions: { + noEmit: false, + emit: ["@typespec/efnext"], + }, + }); +} + +export async function emitWithDiagnostics( + code: string +): Promise<[Record, readonly Diagnostic[]]> { + const runner = await createTypespecCliTestRunner(); + await runner.compileAndDiagnose(code, { + outputDir: "tsp-output", + }); + const emitterOutputDir = "./tsp-output/@typespec/efnext"; + const files = await runner.program.host.readDir(emitterOutputDir); + + const result: Record = {}; + for (const file of files) { + result[file] = (await runner.program.host.readFile(resolvePath(emitterOutputDir, file))).text; + } + return [result, runner.program.diagnostics]; +} + +export async function emit(code: string): Promise> { + const [result, diagnostics] = await emitWithDiagnostics(code); + expectDiagnosticEmpty(diagnostics); + return result; +} + +export async function getProgram( + code: string, + options: { libraries: "Http"[] } = { libraries: [] } +): Promise { + const host = await createTypespecCliTestHost(options); + const wrapper = createTestWrapper(host, { + compilerOptions: { + noEmit: true, + }, + }); + const [_, diagnostics] = await wrapper.compileAndDiagnose(code); + expectDiagnosticEmpty(diagnostics); + return wrapper.program; +} diff --git a/packages/efnext-cli-sketch/tsconfig.json b/packages/efnext-cli-sketch/tsconfig.json new file mode 100644 index 00000000000..11ef603d525 --- /dev/null +++ b/packages/efnext-cli-sketch/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "references": [{ "path": "../compiler/tsconfig.json" }], + "compilerOptions": { + "outDir": "dist", + "rootDir": ".", + "tsBuildInfoFile": "temp/tsconfig.tsbuildinfo", + "jsx": "react-jsx", + "jsxImportSource": "@typespec/efnext", + "sourceMap": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.d.ts", + "src/**/*.tsx", + "generated-defs/**/*.ts", + "test/**/*.ts", + "test/**/*.tsx", + "src/emitter.tsx", + "test/hello.test.tsx" + ] +} diff --git a/packages/efnext-python-sketch/lib/main.tsp b/packages/efnext-python-sketch/lib/main.tsp new file mode 100644 index 00000000000..dc94093e6d9 --- /dev/null +++ b/packages/efnext-python-sketch/lib/main.tsp @@ -0,0 +1 @@ +// model Test {} diff --git a/packages/efnext-python-sketch/package.json b/packages/efnext-python-sketch/package.json new file mode 100644 index 00000000000..aaff0ad5470 --- /dev/null +++ b/packages/efnext-python-sketch/package.json @@ -0,0 +1,74 @@ +{ + "name": "@typespec/efnext-python-sketch", + "version": "0.56.0", + "author": "Microsoft Corporation", + "description": "emitter framework prototype", + "homepage": "https://github.com/microsoft/typespec", + "readme": "https://github.com/microsoft/typespec/blob/main/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", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "default": "./dist/src/index.js" + } + }, + "tspMain": "lib/main.tsp", + "engines": { + "node": ">=18.0.0" + }, + "scripts": { + "clean": "rimraf ./dist ./temp", + "build": "npm run gen-extern-signature && tsc -p . && npm run lint-typespec-library", + "watch": "tsc -p . --watch", + "gen-extern-signature": "tspd --enable-experimental gen-extern-signature .", + "lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit", + "test": "vitest run", + "test:ui": "vitest --ui", + "test:ci": "vitest run --coverage --reporter=junit --reporter=default", + "lint": "eslint . --max-warnings=0", + "lint:fix": "eslint . --fix", + "regen-docs": "tspd doc . --enable-experimental --output-dir ../../docs/emitters/json-schema/reference" + }, + "files": [ + "lib/*.tsp", + "dist/**", + "!dist/test/**" + ], + "peerDependencies": { + "@typespec/compiler": "workspace:~", + "@typespec/http": "workspace:~" + }, + "devDependencies": { + "@types/node": "~18.11.19", + "@typespec/compiler": "workspace:~", + "@typespec/internal-build-utils": "workspace:~", + "@typespec/library-linter": "workspace:~", + "@typespec/tspd": "workspace:~", + "@vitest/coverage-v8": "^1.6.0", + "@vitest/ui": "^1.6.0", + "ajv": "~8.13.0", + "ajv-formats": "~3.0.1", + "c8": "^9.1.0", + "rimraf": "~5.0.7", + "typescript": "~5.4.5", + "vitest": "^1.6.0" + }, + "dependencies": { + "@typespec/efnext": "workspace:~", + "change-case": "~5.4.4", + "prettier": "~3.2.5", + "yaml": "~2.4.2" + } +} diff --git a/packages/efnext-python-sketch/sample/main.tsp b/packages/efnext-python-sketch/sample/main.tsp new file mode 100644 index 00000000000..e40776c82b7 --- /dev/null +++ b/packages/efnext-python-sketch/sample/main.tsp @@ -0,0 +1,35 @@ +import "@typespec/http"; +using TypeSpec.Http; +@service +namespace DemoService { + + @route("/people") + namespace People { + enum PersonKind { + adult, + child, + adultChild + } + + model Person { + firstName: string | int32; + lastName: string; + kind: PersonKind; + special: T; + pets: Pets.Pet[]; + } + + op listPeople(sortBy?: string): Person[]; + } + + + @route("/pets") + namespace Pets { + model Pet { + name: string; + owner?: People.Person; + } + + op listPets(): Pet[]; + } +} \ No newline at end of file diff --git a/packages/efnext-python-sketch/sample/tspconfig.yaml b/packages/efnext-python-sketch/sample/tspconfig.yaml new file mode 100644 index 00000000000..0384d42b2af --- /dev/null +++ b/packages/efnext-python-sketch/sample/tspconfig.yaml @@ -0,0 +1,2 @@ +emit: + - "@typespec/efnext-python-sketch" diff --git a/packages/efnext-python-sketch/src/components/app-folder.tsx b/packages/efnext-python-sketch/src/components/app-folder.tsx new file mode 100644 index 00000000000..4c2ff802914 --- /dev/null +++ b/packages/efnext-python-sketch/src/components/app-folder.tsx @@ -0,0 +1,63 @@ +import { Operation, Type } from "@typespec/compiler"; + +import { Scope, SourceDirectory, SourceFile } from "@typespec/efnext/framework"; +import { TypeDeclaration } from "@typespec/efnext/python"; +import { ClientOperation } from "./client-operation.js"; +import { InitPy } from "./init-py.js"; +import { InternalClientOperation } from "./internal-client-operation.js"; + +export interface AppFolderRecord { + path: string; + moduleName: string; + types: Declaration[]; + operations: Operation[]; + subfolders: AppFolderRecord[]; +} + +export interface AppFolderProps { + folder: AppFolderRecord; +} + +type Declaration = Type & { name: string }; +/** + * This component takes an AppFolder and unpacks it, creating the needed directory, + * source files, etc. + */ +export function AppFolder({ folder }: AppFolderProps) { + const models = folder.types.map((t) => [, "\n"]); // rote conversion of typespec type to python type + + const operations = folder.operations.map((o) => ); + + const internalOperations = folder.operations.map((o) => ( + + )); + + const subfolders = folder.subfolders.map((s) => ); + + return ( + + + + + + {models.length > 0 && ( + + {models} + + )} + {operations.length > 0 && ( + + {operations} + + )} + {internalOperations.length > 0 && ( + + {internalOperations} + + )} + + {subfolders} + + + ); +} diff --git a/packages/efnext-python-sketch/src/components/client-operation.tsx b/packages/efnext-python-sketch/src/components/client-operation.tsx new file mode 100644 index 00000000000..fdf39f8de35 --- /dev/null +++ b/packages/efnext-python-sketch/src/components/client-operation.tsx @@ -0,0 +1,21 @@ +import { Operation } from "@typespec/compiler"; +import { getRefkey } from "@typespec/efnext/framework"; +import { Function, Reference } from "@typespec/efnext/python"; + +interface ClientOperationProps { + operation: Operation; +} + +/** + * Emits a function that now just calls out to the internal one. + * + * Todo: currently hardcodes list, but presumably this should be dynamically imported. + * Need to support so that line is no longer necessary. + */ +export function ClientOperation({ operation }: ClientOperationProps) { + return ( + + return + + ); +} diff --git a/packages/efnext-python-sketch/src/components/init-py.tsx b/packages/efnext-python-sketch/src/components/init-py.tsx new file mode 100644 index 00000000000..5aba0060fe3 --- /dev/null +++ b/packages/efnext-python-sketch/src/components/init-py.tsx @@ -0,0 +1,43 @@ +import { code } from "@typespec/compiler/emitter-framework"; +import { useNamePolicy } from "@typespec/efnext/framework"; +import { AppFolderRecord } from "./app-folder.js"; + +export interface InitPyProps { + folder: AppFolderRecord; +} +/** + * Creates an InitPy for a folder. + * Todo: could be more generalized by using to get the type names, which would + * allow us to change the emitting names at any point. + */ +export function InitPy({ folder }: InitPyProps) { + const namer = useNamePolicy(); + let imports = []; + let all = []; + + for (const subfolder of folder.subfolders) { + imports.push(`from .${subfolder.path} import *`); + all.push(subfolder.path); + } + + if (folder.types.length > 0) { + const typeNames = folder.types.map((t) => namer.getName(t, "class")); + imports.push(`from models import ${typeNames.join(",")}`); + all = all.concat(typeNames); + } + + if (folder.operations.length > 0) { + const opNames = folder.operations.map((t) => namer.getName(t, "function")); + imports.push(`from operations import ${opNames.join(",")}`); + all = all.concat(opNames); + } + + return code` + ${imports.join("\n")} + + __all__ = [ + ${all.map((v) => `"${v}"`).join("\n") /**Probably need to fix this in code? */} + ] + + `; +} diff --git a/packages/efnext-python-sketch/src/components/internal-client-operation.tsx b/packages/efnext-python-sketch/src/components/internal-client-operation.tsx new file mode 100644 index 00000000000..e807300012b --- /dev/null +++ b/packages/efnext-python-sketch/src/components/internal-client-operation.tsx @@ -0,0 +1,19 @@ +import { Operation } from "@typespec/compiler"; +import { getRefkey, useNamePolicy } from "@typespec/efnext/framework"; +import { Function } from "@typespec/efnext/python"; + +export interface InternalClientOperationProps { + operation: Operation; +} +/** + * Emits an internal thing that will eventually have a declaration. + */ +export function InternalClientOperation({ operation }: InternalClientOperationProps) { + const namer = useNamePolicy(); + const name = `_` + namer.getName(operation, "function"); + return ( + + raise NotImplementedError + + ); +} diff --git a/packages/efnext-python-sketch/src/index.tsx b/packages/efnext-python-sketch/src/index.tsx new file mode 100644 index 00000000000..31303a3e1b5 --- /dev/null +++ b/packages/efnext-python-sketch/src/index.tsx @@ -0,0 +1,87 @@ +import { + EmitContext, + Enum, + Model, + Namespace, + Program, + Type, + Union, + navigateType, +} from "@typespec/compiler"; +import { EmitOutput, emit } from "@typespec/efnext/framework"; +import { pythonNamePolicy } from "@typespec/efnext/python"; +import { getAllHttpServices } from "@typespec/http"; +import { AppFolder, AppFolderRecord } from "./components/app-folder.js"; + +export async function $onEmit(context: EmitContext) { + if (context.program.compilerOptions.noEmit) { + return; + } + + // queryApp walks the type graph and assembles the AppFolder structure. + const rootFolder = queryApp(context); + await emit( + context, + + + + ); +} + +// use compiler APIs to assemble this structure. +function queryApp({ program }: EmitContext) { + // todo: ignoring diagnostics for now + const services = getAllHttpServices(program)[0]; + if (services.length === 0) { + throw new Error("No service found"); + } + + const service = services[0]; + const models = new Map(); + + // find all models within the service namespace + // and organize them by the namespace they're in. + function emitType(type: Model | Enum | Union) { + if (!models.get(type.namespace!)) { + models.set(type.namespace!, []); + } + + const ms = models.get(type.namespace!)!; + ms.push(type); + } + + navigateType( + service.namespace, + { + model: emitType, + enum: emitType, + union: emitType, + }, + {} + ); + + return getFolderForNamespace(program, service.namespace, "./", models); +} + +function getFolderForNamespace( + program: Program, + namespace: Namespace, + path: string, + models: Map +): AppFolderRecord { + const rootFolder: AppFolderRecord = { + path, + types: findTypesInNamespace(namespace, models), + moduleName: namespace.name, + operations: [...namespace.operations.values()], + subfolders: [...namespace.namespaces.values()].map((n) => + getFolderForNamespace(program, n, n.name, models) + ), + }; + + return rootFolder; +} + +function findTypesInNamespace(root: Namespace, models: Map) { + return (models.get(root) as Model[]) ?? []; +} diff --git a/packages/efnext-python-sketch/src/testing/index.ts b/packages/efnext-python-sketch/src/testing/index.ts new file mode 100644 index 00000000000..453a3317122 --- /dev/null +++ b/packages/efnext-python-sketch/src/testing/index.ts @@ -0,0 +1,8 @@ +import { resolvePath } from "@typespec/compiler"; +import { createTestLibrary, TypeSpecTestLibrary } from "@typespec/compiler/testing"; +import { fileURLToPath } from "url"; + +export const TestLibrary: TypeSpecTestLibrary = createTestLibrary({ + name: "@typespec/efnext", + packageRoot: resolvePath(fileURLToPath(import.meta.url), "../../../"), +}); diff --git a/packages/efnext-python-sketch/test/component-utils.tsx b/packages/efnext-python-sketch/test/component-utils.tsx new file mode 100644 index 00000000000..ada0928d08a --- /dev/null +++ b/packages/efnext-python-sketch/test/component-utils.tsx @@ -0,0 +1,31 @@ +import { EmitOutput, RenderedTreeNode, SourceFile, render } from "@typespec/efnext/framework"; +import { format } from "prettier"; +import { assert } from "vitest"; + +async function prepareExpected(expected: string) { + const expectedRoot = ( + + + {expected} + + + ); + + const rendered = await render(expectedRoot); + const raw = (rendered as any).flat(Infinity).join(""); + + return format(raw, { parser: "typescript" }); +} + +async function prepareActual(actual: RenderedTreeNode) { + const raw = (actual as any).flat(Infinity).join(""); + + return format(raw, { parser: "typescript" }); +} + +export async function assertEqual(actual: RenderedTreeNode, expected: string) { + const actualFormatted = await prepareActual(actual); + const expectedFormatted = await prepareExpected(expected); + + assert.equal(actualFormatted, expectedFormatted); +} diff --git a/packages/efnext-python-sketch/test/test-host.ts b/packages/efnext-python-sketch/test/test-host.ts new file mode 100644 index 00000000000..1c7641ab2d4 --- /dev/null +++ b/packages/efnext-python-sketch/test/test-host.ts @@ -0,0 +1,69 @@ +import { Diagnostic, Program, resolvePath } from "@typespec/compiler"; +import { + createTestHost, + createTestWrapper, + expectDiagnosticEmpty, +} from "@typespec/compiler/testing"; +import { HttpTestLibrary } from "@typespec/http/testing"; +import { TestLibrary } from "../src/testing/index.js"; + +export async function createTypespecCliTestHost( + options: { libraries: "Http"[] } = { libraries: [] } +) { + const libraries = [TestLibrary]; + if (options.libraries.includes("Http")) { + libraries.push(HttpTestLibrary); + } + return createTestHost({ + libraries, + }); +} + +export async function createTypespecCliTestRunner() { + const host = await createTypespecCliTestHost(); + + return createTestWrapper(host, { + compilerOptions: { + noEmit: false, + emit: ["@typespec/efnext"], + }, + }); +} + +export async function emitWithDiagnostics( + code: string +): Promise<[Record, readonly Diagnostic[]]> { + const runner = await createTypespecCliTestRunner(); + await runner.compileAndDiagnose(code, { + outputDir: "tsp-output", + }); + const emitterOutputDir = "./tsp-output/@typespec/efnext"; + const files = await runner.program.host.readDir(emitterOutputDir); + + const result: Record = {}; + for (const file of files) { + result[file] = (await runner.program.host.readFile(resolvePath(emitterOutputDir, file))).text; + } + return [result, runner.program.diagnostics]; +} + +export async function emit(code: string): Promise> { + const [result, diagnostics] = await emitWithDiagnostics(code); + expectDiagnosticEmpty(diagnostics); + return result; +} + +export async function getProgram( + code: string, + options: { libraries: "Http"[] } = { libraries: [] } +): Promise { + const host = await createTypespecCliTestHost(options); + const wrapper = createTestWrapper(host, { + compilerOptions: { + noEmit: true, + }, + }); + const [_, diagnostics] = await wrapper.compileAndDiagnose(code); + expectDiagnosticEmpty(diagnostics); + return wrapper.program; +} diff --git a/packages/efnext-python-sketch/tsconfig.json b/packages/efnext-python-sketch/tsconfig.json new file mode 100644 index 00000000000..7fd06f23e52 --- /dev/null +++ b/packages/efnext-python-sketch/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "references": [{ "path": "../compiler/tsconfig.json" }], + "compilerOptions": { + "outDir": "dist", + "rootDir": ".", + "tsBuildInfoFile": "temp/tsconfig.tsbuildinfo", + "jsx": "react-jsx", + "jsxImportSource": "@typespec/efnext", + "sourceMap": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.d.ts", + "src/**/*.tsx", + "generated-defs/**/*.ts", + "test/**/*.ts", + "test/**/*.tsx", + "src/index.tsx", + "test/hello.test.tsx" + ] +} diff --git a/packages/efnext/lib/main.tsp b/packages/efnext/lib/main.tsp new file mode 100644 index 00000000000..dc94093e6d9 --- /dev/null +++ b/packages/efnext/lib/main.tsp @@ -0,0 +1 @@ +// model Test {} diff --git a/packages/efnext/package.json b/packages/efnext/package.json new file mode 100644 index 00000000000..69c1c8fe01f --- /dev/null +++ b/packages/efnext/package.json @@ -0,0 +1,112 @@ +{ + "name": "@typespec/efnext", + "version": "0.56.0", + "author": "Microsoft Corporation", + "description": "emitter framework prototype", + "homepage": "https://github.com/microsoft/typespec", + "readme": "https://github.com/microsoft/typespec/blob/main/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", + "json schema" + ], + "type": "module", + "main": "dist/src/index.js", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "default": "./dist/src/index.js" + }, + "./testing": { + "types": "./dist/src/testing/index.d.ts", + "default": "./dist/src/testing/index.js" + }, + "./framework": { + "types": "./dist/src/framework/index.d.ts", + "default": "./dist/src/framework/index.js" + }, + "./python": { + "types": "./dist/src/python/index.d.ts", + "default": "./dist/src/python/index.js" + }, + "./typescript": { + "types": "./dist/src/typescript/index.d.ts", + "default": "./dist/src/typescript/index.js" + }, + "./jsx-runtime": { + "types": "./dist/src/framework/core/jsx.d.ts", + "default": "./dist/src/framework/core/jsx.js" + }, + "./jsx-dev-runtime": { + "types": "./dist/src/framework/core/jsx.d.ts", + "default": "./dist/src/framework/core/jsx.js" + } + }, + "tspMain": "lib/main.tsp", + "engines": { + "node": ">=18.0.0" + }, + "imports": { + "#jsx/jsx-runtime": { + "default": "./dist/src/framework/core/jsx.js" + }, + "#jsx/jsx-dev-runtime": { + "default": "./dist/src/framework/core/jsx.js" + }, + "#typespec/emitter/python": { + "default": "./dist/src/python/index.js" + }, + "#typespec/emitter/core": { + "default": "./dist/src/framework/index.js" + } + }, + "scripts": { + "clean": "rimraf ./dist ./temp", + "build": "npm run gen-extern-signature && tsc -p . && npm run lint-typespec-library", + "watch": "tsc -p . --watch", + "gen-extern-signature": "tspd --enable-experimental gen-extern-signature .", + "lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit", + "test": "vitest run", + "test:ui": "vitest --ui", + "test:ci": "vitest run --coverage --reporter=junit --reporter=default", + "lint": "eslint . --max-warnings=0", + "lint:fix": "eslint . --fix", + "regen-docs": "tspd doc . --enable-experimental --output-dir ../../docs/emitters/json-schema/reference" + }, + "files": [ + "lib/*.tsp", + "dist/**", + "!dist/test/**" + ], + "peerDependencies": { + "@typespec/compiler": "workspace:~", + "@typespec/http": "workspace:~" + }, + "devDependencies": { + "@types/node": "~18.11.19", + "@typespec/compiler": "workspace:~", + "@typespec/internal-build-utils": "workspace:~", + "@typespec/library-linter": "workspace:~", + "@typespec/tspd": "workspace:~", + "@vitest/coverage-v8": "^1.6.0", + "@vitest/ui": "^1.6.0", + "ajv": "~8.13.0", + "ajv-formats": "~3.0.1", + "c8": "^9.1.0", + "rimraf": "~5.0.7", + "typescript": "~5.4.5", + "vitest": "^1.6.0" + }, + "dependencies": { + "change-case": "~5.4.4", + "prettier": "~3.2.5", + "yaml": "~2.4.2" + } +} diff --git a/packages/efnext/sample/main.tsp b/packages/efnext/sample/main.tsp new file mode 100644 index 00000000000..e40776c82b7 --- /dev/null +++ b/packages/efnext/sample/main.tsp @@ -0,0 +1,35 @@ +import "@typespec/http"; +using TypeSpec.Http; +@service +namespace DemoService { + + @route("/people") + namespace People { + enum PersonKind { + adult, + child, + adultChild + } + + model Person { + firstName: string | int32; + lastName: string; + kind: PersonKind; + special: T; + pets: Pets.Pet[]; + } + + op listPeople(sortBy?: string): Person[]; + } + + + @route("/pets") + namespace Pets { + model Pet { + name: string; + owner?: People.Person; + } + + op listPets(): Pet[]; + } +} \ No newline at end of file diff --git a/packages/efnext/sample/tspconfig.yaml b/packages/efnext/sample/tspconfig.yaml new file mode 100644 index 00000000000..b62569d15c9 --- /dev/null +++ b/packages/efnext/sample/tspconfig.yaml @@ -0,0 +1,2 @@ +emit: + - "@typespec/efnext" diff --git a/packages/efnext/src/framework/components/declaration.tsx b/packages/efnext/src/framework/components/declaration.tsx new file mode 100644 index 00000000000..0a21843f099 --- /dev/null +++ b/packages/efnext/src/framework/components/declaration.tsx @@ -0,0 +1,32 @@ +import { ComponentChildren } from "#jsx/jsx-runtime"; +import { BinderContext, OutputDeclaration } from "../core/binder.js"; +import { createContext, useContext } from "../core/context.js"; +import { ScopeContext } from "./scope.js"; + +const DeclarationContext = createContext(); + +export interface DeclarationProps { + name: string; + refkey?: unknown; + children?: ComponentChildren; +} +export function Declaration({ name, children, refkey}: DeclarationProps) { + if (refkey === undefined) { + // todo: use FQN + refkey = name; + } + const currentDeclaration = useContext(DeclarationContext); + if (currentDeclaration) { + throw new Error("Cannot nest declarations"); + } + + const binder = useContext(BinderContext) + if (!binder) { + throw new Error("Need binder context to create declarations"); + } + const scope = useContext(ScopeContext); + const declaration = binder.createDeclaration(name, scope, refkey); + return + {children} + +} \ No newline at end of file diff --git a/packages/efnext/src/framework/components/emit-output.tsx b/packages/efnext/src/framework/components/emit-output.tsx new file mode 100644 index 00000000000..cd5916b03f8 --- /dev/null +++ b/packages/efnext/src/framework/components/emit-output.tsx @@ -0,0 +1,19 @@ +import { ComponentChildren } from "#jsx/jsx-runtime"; +import { BinderContext, createOutputBinder } from "../core/binder.js"; +import { NamePolicy, createNamePolicy } from "../core/name-policy.js"; + +export interface EmitOutputProps { + namePolicy?: NamePolicy + children?: ComponentChildren +} + +export function EmitOutput({ namePolicy, children }: EmitOutputProps) { + namePolicy ??= createNamePolicy((type) => "name" in type && typeof type.name === "string" ? type.name : ""); + + const binder = createOutputBinder(); + return + + {children} + + +} diff --git a/packages/efnext/src/framework/components/indent.tsx b/packages/efnext/src/framework/components/indent.tsx new file mode 100644 index 00000000000..629126c1f61 --- /dev/null +++ b/packages/efnext/src/framework/components/indent.tsx @@ -0,0 +1,39 @@ +import { ComponentChildren } from "#jsx/jsx-runtime"; +import { createContext, useContext } from "../core/context.js"; +import { printChildren } from "../core/render.js"; +import { usePrinter } from "../core/use-printer.js"; + +export const IndentContext = createContext(); + +export interface IndentProps { + children?: ComponentChildren; + indent?: string; +} + +export interface IndentState { + level: number; + indent: string; +} + +export function Indent({ indent, children }: IndentProps) { + const previousIndent = useContext(IndentContext) ?? { + level: 0, + indent: indent ?? " ", + }; + + const currentIndent = { + level: previousIndent.level + 1, + indent: indent ?? previousIndent.indent, + }; + + usePrinter((node) => { + const text = printChildren(node); + const indented = text + .split("\n") + .map((l) => (l.trim().length === 0 ? l : currentIndent.indent + l)) + .join("\n"); + return indented; + }); + + return {children}; +} diff --git a/packages/efnext/src/framework/components/index.tsx b/packages/efnext/src/framework/components/index.tsx new file mode 100644 index 00000000000..58ce313aafc --- /dev/null +++ b/packages/efnext/src/framework/components/index.tsx @@ -0,0 +1,7 @@ +export * from "./emit-output.js"; +export * from "./source-file.js"; +export * from "./declaration.js"; +export * from "./indent.js"; +export * from "./scope.js"; +export * from "./source-directory.js"; +export * from "./source-file.js"; \ No newline at end of file diff --git a/packages/efnext/src/framework/components/scope.tsx b/packages/efnext/src/framework/components/scope.tsx new file mode 100644 index 00000000000..7e0a77872ac --- /dev/null +++ b/packages/efnext/src/framework/components/scope.tsx @@ -0,0 +1,23 @@ +import { createContext, useContext } from "../core/context.js"; +import { BinderContext, LocalScope } from "../core/binder.js"; +import { ComponentChildren } from "#jsx/jsx-runtime"; + +export const ScopeContext = createContext(); + +export interface ScopeProps { + name: string; + children?: ComponentChildren; + meta?: unknown; +} + +export function Scope({ name, children, meta }: ScopeProps) { + const binder = useContext(BinderContext); + if (!binder) { + throw new Error("Scope requires binder context"); + } + const currentScope = useContext(ScopeContext); + const newScope = binder.createLocalScope(name, currentScope, meta); + return + {children} + ; +} \ No newline at end of file diff --git a/packages/efnext/src/framework/components/source-directory.tsx b/packages/efnext/src/framework/components/source-directory.tsx new file mode 100644 index 00000000000..871b5aa79a7 --- /dev/null +++ b/packages/efnext/src/framework/components/source-directory.tsx @@ -0,0 +1,39 @@ +import { ComponentChildren } from "#jsx/jsx-runtime"; +import { BinderContext, GlobalScope, ModuleScope } from "../core/binder.js"; +import { createContext, useContext } from "../core/context.js"; +import { getRenderContext } from "../core/render.js"; +import { ScopeContext } from "./scope.js"; + +export interface SourceDirectoryProps { + path: string; + children?: ComponentChildren[]; +} + +interface SourceDirectoryState { + scope: ModuleScope; +} + +export const SourceDirectoryContext = createContext(); + +export function SourceDirectory({ path, children }: SourceDirectoryProps) { + const renderContext = getRenderContext(); + renderContext.meta!.sourceDirectory = { path }; + + const binder = useContext(BinderContext); + if (!binder) { + throw new Error("Scope requires binder context"); + } + + const currentScope = useContext(ScopeContext); + const scope = binder.createModuleScope(path, currentScope?.parent as ModuleScope | GlobalScope); + + const sourceDirectoryState: SourceDirectoryState = { + scope, + }; + + return ( + + {children} + + ); +} diff --git a/packages/efnext/src/framework/components/source-file.tsx b/packages/efnext/src/framework/components/source-file.tsx new file mode 100644 index 00000000000..13f2354e293 --- /dev/null +++ b/packages/efnext/src/framework/components/source-file.tsx @@ -0,0 +1,102 @@ +import { ComponentChildren } from "#jsx/jsx-runtime"; +import { join } from "path"; +import { BinderContext, GlobalScope, ModuleScope } from "../core/binder.js"; +import { createContext, useContext } from "../core/context.js"; +import { MetaNode } from "../core/metatree.js"; +import { getRenderContext } from "../core/render.js"; +import { useResolved } from "../core/use-resolved.js"; +import { ScopeContext } from "./scope.js"; + +export interface SourceFileProps { + path: string; + filetype: "typescript" | "python"; + children?: ComponentChildren[]; +} + +interface ImportRecord { + importPath: string; + name: string; + pathKind?: "relative" | "absolute"; +} + +interface SourceFileState { + imports: Map; + addImport(record: ImportRecord): void; + scope: ModuleScope; +} + +export const SourceFileContext = createContext(); + +export function SourceFile({ path, filetype, children }: SourceFileProps) { + const renderContext = getRenderContext(); + const fullPath = resolvePath(renderContext.meta, path); + renderContext.meta!.sourceFile = { path: fullPath, fileType: filetype }; + + const binder = useContext(BinderContext); + if (!binder) { + throw new Error("Scope requires binder context"); + } + + const currentScope = useContext(ScopeContext); + const scope = binder.createModuleScope(path, currentScope?.parent as ModuleScope | GlobalScope); + const imports: Map = new Map(); + const sourceFileState: SourceFileState = { + scope, + imports, + addImport(record) { + if (!imports.has(record.importPath)) { + imports.set(record.importPath, []); + } + + const records = imports.get(record.importPath)!; + + // todo: consider making this a set. + if (records.find((r) => r.name === record.name)) { + return; + } + + records.push(record); + }, + }; + + const ImportContainer = useResolved(() => { + let importString = ""; + for (const [importPath, records] of imports) { + if (filetype === "typescript") { + const isRelativePath = records.some( + (r) => r.pathKind === undefined || r.pathKind === "relative" + ); + const pathPrefix = isRelativePath ? "./" : ""; + importString += `import {${records.map((r) => r.name).join(", ")}} from "${pathPrefix}${importPath.replace(/\.ts$/, ".js")}"\n`; + } + + if (filetype === "python") { + importString += `from ${importPath.replace(/\.py$/, "")} import ${records.map((r) => r.name).join(", ")}\n`; + } + } + + return <>{importString}; + }); + + return ( + + +
+ {children} +
+ ); +} + +function resolvePath(node: MetaNode | undefined, path: string) { + const parent = node?.parent; + + if (!parent) { + return path; + } + + if (parent.sourceDirectory) { + return resolvePath(parent, join(parent.sourceDirectory.path, path)); + } + + return resolvePath(parent, path); +} diff --git a/packages/efnext/src/framework/core/binder.ts b/packages/efnext/src/framework/core/binder.ts new file mode 100644 index 00000000000..e7fbae9d184 --- /dev/null +++ b/packages/efnext/src/framework/core/binder.ts @@ -0,0 +1,217 @@ +import { createContext } from "./context.js"; + +export interface OutputDeclaration { + name: string; + scope: AnyScope; + refkey: unknown; +} + +export interface ScopeBase { + kind: string; + bindings: Map; + bindingsByKey: Map; + children: Map; + parent: AnyScope | undefined; + binder: Binder; + meta: any; +} + +// could include package scope +export type AnyScope = LocalScope | ModuleScope | GlobalScope; + +export interface LocalScope extends ScopeBase { + kind: "local"; + name: string; + parent: AnyScope; + children: Map; +} + +export interface ModuleScope extends ScopeBase { + kind: "module"; + path: string; + /** could probably grow to include namespace etc. */ + children: Map; + parent: GlobalScope | ModuleScope; // dunno if modules should be able to nest... +} + +export interface GlobalScope extends ScopeBase { + kind: "global"; + parent: undefined; + children: Map; +} + +export const BinderContext = createContext(); + +export interface Binder { + createLocalScope(name: string, parent: LocalScope | undefined, meta?: unknown): LocalScope; + createModuleScope( + path: string, + parent: GlobalScope | ModuleScope | undefined, + meta?: unknown + ): ModuleScope; + createDeclaration(name: string, scope?: LocalScope, refkey?: unknown): void; + resolveDeclarationByKey( + currentScope: LocalScope, + refkey: unknown + ): ResolutionResult | ResolutionFailure; + resolveOrWaitForDeclaration( + currentScope: LocalScope, + refkey: unknown + ): Promise; +} + +export interface ResolutionFailure { + resolved: false; +} + +export interface ResolutionResult { + resolved: true; + targetDeclaration: OutputDeclaration; + pathUp: AnyScope[]; + pathDown: AnyScope[]; + commonScope: AnyScope | undefined; +} + +export function createOutputBinder(): Binder { + const binder: Binder = { + createLocalScope, + createModuleScope, + createDeclaration, + resolveDeclarationByKey, + resolveOrWaitForDeclaration, + }; + + const globalScope: GlobalScope = { + kind: "global", + bindings: new Map(), + bindingsByKey: new Map(), + children: new Map(), + parent: undefined, + binder, + meta: {}, + }; + + const knownDeclarations = new Map(); + const waitingDeclarations = new Map void)[]>(); + return binder; + + function createLocalScope( + name: string, + parent: AnyScope = globalScope, + meta?: unknown + ): LocalScope { + const scope: LocalScope = { + kind: "local", + name, + bindings: new Map(), + bindingsByKey: new Map(), + children: new Map(), + parent, + binder, + meta, + }; + + if (parent) { + parent.children.set(name, scope); + } + + return scope; + } + + function createModuleScope( + path: string, + parent: GlobalScope | ModuleScope = globalScope, + meta?: unknown + ): ModuleScope { + const scope: ModuleScope = { + kind: "module", + path, + bindings: new Map(), + bindingsByKey: new Map(), + children: new Map(), + parent: parent, + binder, + meta, + }; + + globalScope.children.set(path, scope); + + return scope; + } + + function createDeclaration(name: string, scope: AnyScope = globalScope, refkey: unknown) { + const declaration: OutputDeclaration = { + name, + scope, + refkey, + }; + + const targetScope = scope ? scope : globalScope; + targetScope.bindings.set(name, declaration); + knownDeclarations.set(refkey, declaration); + if (waitingDeclarations.has(refkey)) { + const cbs = waitingDeclarations.get(refkey)!; + for (const cb of cbs) { + cb(declaration); + } + } + return declaration; + } + + // todo: handle key not yet found because its yet to be declared + function resolveDeclarationByKey( + currentScope: LocalScope, + key: unknown + ): ResolutionFailure | ResolutionResult { + const targetDeclaration = knownDeclarations.get(key); + if (!targetDeclaration) { + return { resolved: false }; + } + const targetScope = targetDeclaration.scope; + const targetChain = scopeChain(targetScope); + const currentChain = scopeChain(currentScope); + let diffStart = 0; + while ( + targetChain[diffStart] && + currentChain[diffStart] && + targetChain[diffStart] === currentChain[diffStart] + ) { + diffStart++; + } + + const pathUp = currentChain.slice(diffStart); + const pathDown = targetChain.slice(diffStart); + const commonScope = targetChain[diffStart - 1] ?? null; + + return { resolved: true, pathUp, pathDown, commonScope, targetDeclaration }; + } + + async function resolveOrWaitForDeclaration(scope: LocalScope, refkey: unknown) { + if (!knownDeclarations.has(refkey)) { + await declarationAvailable(refkey); + } + + return resolveDeclarationByKey(scope, refkey); + } + + function declarationAvailable(refkey: unknown) { + if (!waitingDeclarations.has(refkey)) { + waitingDeclarations.set(refkey, []); + } + + const waitingList = waitingDeclarations.get(refkey); + return new Promise((resolve) => { + waitingList?.push(resolve); + }); + } + + function scopeChain(scope: AnyScope | undefined) { + const chain = []; + while (scope) { + chain.unshift(scope); + scope = scope.parent; + } + + return chain; + } +} diff --git a/packages/efnext/src/framework/core/code.ts b/packages/efnext/src/framework/core/code.ts new file mode 100644 index 00000000000..b5a0a6995f8 --- /dev/null +++ b/packages/efnext/src/framework/core/code.ts @@ -0,0 +1,60 @@ +import { ComponentChildren, jsx } from "#jsx/jsx-runtime"; +import { Indent } from "../components/indent.js"; + +const startWhitespaceRe = /^\s*/; +const endWhitespaceRe = /[\r\n]+\s*$/; +const endIndentRe = /[ \t]*$/; + +/** + * This tagged template function takes literal strings and component substitutions + * and handles indent state and merging the literal strings with substitutions into + * ComponentChildren that can be nested within a JSX template. + */ +export function code(strs: TemplateStringsArray, ...subs: ComponentChildren[]): ComponentChildren { + // dedent... + const children: ComponentChildren = []; + + if (strs[0][0] !== "\n") { + throw new Error("Code template must begin with a line break"); + } + + const processedStrs = [strs[0].slice(1), ...strs.slice(1, -1)]; + if (strs.length > 1) { + processedStrs.push(strs[strs.length - 1].replace(/[\r\n\s]+$/, "")); + } + const firstLineIndent = processedStrs[0].match(startWhitespaceRe)![0]; + const result = processTemplateString(processedStrs[0], firstLineIndent); + let subIndent = result.indent; + children.push(result.trimmed); + + for (let i = 0; i < subs.length; i++) { + const item = subs[i]; + children.push(jsx(Indent, { indent: subIndent, children: item })); + const result = processTemplateString(processedStrs[i + 1], firstLineIndent); + subIndent = result.indent; + children.push(result.trimmed); + } + + return children; +} + +function processTemplateString(str: string, firstLineIndent: string) { + str = dedent(str, firstLineIndent); + const lastLineIndentMatch = str.match(endWhitespaceRe); + if (lastLineIndentMatch) { + return { + indent: str.match(endIndentRe)![0], + trimmed: str.replace(endIndentRe, ""), + }; + } else { + return { indent: "", trimmed: str }; + } +} +function dedent(str: string, firstLineIndent: string) { + const lines = str.split("\n"); + const dedentedLines = lines.map((line) => + line.startsWith(firstLineIndent) ? line.slice(firstLineIndent.length) : line + ); + + return dedentedLines.join("\n"); +} diff --git a/packages/efnext/src/framework/core/context.ts b/packages/efnext/src/framework/core/context.ts new file mode 100644 index 00000000000..27b64d48e70 --- /dev/null +++ b/packages/efnext/src/framework/core/context.ts @@ -0,0 +1,44 @@ +import { ComponentChildren, FunctionComponent } from "#jsx/jsx-runtime"; +import { getRenderContext } from "./render.js"; + +export interface Context { + id: symbol; + Provider: FunctionComponent; + currentValue: T | undefined; +} + +interface ProviderProps { + value?: unknown; + children?: ComponentChildren; +} +export function createContext(defaultValue?: T): Context { + const id = Symbol(); + + return { + id, + currentValue: defaultValue, + Provider({ value, children }: ProviderProps) { + const renderContext = getRenderContext(); + renderContext.meta!.contextId = id; + renderContext.meta!.contextValue = value; + + return children; + }, + }; +} + +export function useContext(context: Context): T | undefined { + const renderContext = getRenderContext(); + + // context must come from a parent + let metaNode = renderContext.meta!.parent; + while (metaNode) { + if (metaNode.contextId === context.id) { + return metaNode.contextValue as T; + } + + metaNode = metaNode.parent; + } + + return undefined; +} diff --git a/packages/efnext/src/framework/core/emit.ts b/packages/efnext/src/framework/core/emit.ts new file mode 100644 index 00000000000..9a36af93fb7 --- /dev/null +++ b/packages/efnext/src/framework/core/emit.ts @@ -0,0 +1,12 @@ +import { SourceNode } from "#jsx/jsx-runtime"; +import { EmitContext, emitFile, joinPaths } from "@typespec/compiler"; +import { renderToSourceFiles } from "./render.js"; + +// todo: this should be handled when returning components from OnEmit +export async function emit(context: EmitContext, node: SourceNode) { + const rendered = await renderToSourceFiles(node, { format: false }); + for (const file of rendered) { + file.path = joinPaths(context.emitterOutputDir, file.path); + await emitFile(context.program, file); + } +} diff --git a/packages/efnext/src/framework/core/index.ts b/packages/efnext/src/framework/core/index.ts new file mode 100644 index 00000000000..9e6ae0635cc --- /dev/null +++ b/packages/efnext/src/framework/core/index.ts @@ -0,0 +1,10 @@ +export * from "./binder.js"; +export * from "./code.js"; +export * from "./context.js"; +export * from "./emit.js"; +export * from "./metatree.js"; +export * from "./name-policy.js"; +export * from "./refkeyer.js"; +export * from "./render.js"; +export * from "./use-printer.js"; +export * from "./use-resolved.js"; diff --git a/packages/efnext/src/framework/core/jsx.ts b/packages/efnext/src/framework/core/jsx.ts new file mode 100644 index 00000000000..516e589cea7 --- /dev/null +++ b/packages/efnext/src/framework/core/jsx.ts @@ -0,0 +1,50 @@ +export interface FunctionComponent { + // seems like react lets you just return children here, so even + // though my instinct is to return SourceNode here, that seems to not work. + (props: ComponentProps): ComponentChildren; +} + +export interface SourceNode { + type: FunctionComponent | string; + props: ComponentProps; +} + +export function isSourceNode(node: any): node is SourceNode { + return typeof node === "object" && "type" in node && "props" in node; +} + +export type ComponentChild = + | SourceNode + | string + | number + | boolean + | null + | undefined + | Promise; +export type ComponentChildren = ComponentChild | ComponentChild[]; +export type ComponentProps = Record & { children?: ComponentChildren }; + +export function jsx(component: FunctionComponent, props?: Record): SourceNode { + return createSourceElement(component, props); +} + +export function Fragment(props: ComponentProps): ComponentChildren { + return props.children; +} + +export function jsxs(component: FunctionComponent, props?: Record): SourceNode { + return createSourceElement(component, props); +} + +export const jsxDEV = jsx; +export const jsxsDEV = jsxs; + +function createSourceElement( + type: FunctionComponent | string, + props?: Record +): SourceNode { + return { + type, + props: props ?? {}, + }; +} diff --git a/packages/efnext/src/framework/core/metatree.ts b/packages/efnext/src/framework/core/metatree.ts new file mode 100644 index 00000000000..5afa2176a04 --- /dev/null +++ b/packages/efnext/src/framework/core/metatree.ts @@ -0,0 +1,35 @@ +import { FunctionComponent } from "#jsx/jsx-runtime"; +import { RenderedTreeNode } from "./render.js"; + +const metaNodes = new Map(); + +export interface MetaNode { + node: RenderedTreeNode; + type?: FunctionComponent; + parent?: MetaNode; + contextId?: symbol; + contextValue?: unknown; + sourceFile?: SourceFileMetadata; + sourceDirectory?: SourceDirectoryMetadata; +} + +export interface SourceFileMetadata { + path: string; + fileType: "typescript" | "python"; +} + +export interface SourceDirectoryMetadata { + path: string; +} + +export function getMeta(node: RenderedTreeNode) { + if (!metaNodes.has(node)) { + const newMetaNode: MetaNode = { + node, + }; + metaNodes.set(node, newMetaNode); + return newMetaNode; + } + + return metaNodes.get(node)!; +} diff --git a/packages/efnext/src/framework/core/name-policy.tsx b/packages/efnext/src/framework/core/name-policy.tsx new file mode 100644 index 00000000000..859de79dd15 --- /dev/null +++ b/packages/efnext/src/framework/core/name-policy.tsx @@ -0,0 +1,58 @@ +import { ComponentChildren, FunctionComponent } from "#jsx/jsx-runtime"; +import { Type } from "@typespec/compiler"; +import { createContext, useContext } from "./context.js"; + +export interface RenamerCallback { + (type: Type, kind: TKinds): string; +} + +export interface NamePolicyProviderProps { + children?: ComponentChildren; +} + +export interface Renamer { + getName(type: Type, kind: TKinds): string; + setName(type: Type, name: string): void; +} + +export interface NamePolicy { + renamer: Renamer; + Provider: FunctionComponent; +} + +export const RenamingPolicyContext = createContext(); +export function useNamePolicy() { + // it's possible this is undefined if you don't use the component + // which initializes the default naming policy, but that seems rare. + return useContext(RenamingPolicyContext)!; +} + +export function createNamePolicy(namer: RenamerCallback) { + const overrides = new WeakMap(); + + const renamer: Renamer = { + getName(type, kind) { + if (overrides.has(type)) { + return overrides.get(type)!; + } + + return namer(type, kind); + }, + setName(type: Type, name: string) { + overrides.set(type, name); + }, + }; + + function Provider(props: NamePolicyProviderProps) { + return ( + + {props.children} + + ); + } + + return { + renamer, + Provider, + }; +} diff --git a/packages/efnext/src/framework/core/refkeyer.ts b/packages/efnext/src/framework/core/refkeyer.ts new file mode 100644 index 00000000000..5bd754eea90 --- /dev/null +++ b/packages/efnext/src/framework/core/refkeyer.ts @@ -0,0 +1,16 @@ +import { Type } from "@typespec/compiler"; +import { CustomKeyMap } from "@typespec/compiler/emitter-framework"; + +const typeKeyer = CustomKeyMap.objectKeyer(); +const refkeyKeyer = ([type, variant]: [Type, string]) => `${typeKeyer.getKey(type)}-${variant}`; +const refkeys = new CustomKeyMap<[Type, string], symbol>(refkeyKeyer); +export function getRefkey(type: Type, variant: string) { + let key = refkeys.get([type, variant]); + if (key) { + return key; + } + + key = Symbol(); + refkeys.set([type, variant], key); + return key; +} diff --git a/packages/efnext/src/framework/core/render.ts b/packages/efnext/src/framework/core/render.ts new file mode 100644 index 00000000000..1035a11b56d --- /dev/null +++ b/packages/efnext/src/framework/core/render.ts @@ -0,0 +1,213 @@ +import { ComponentChild, ComponentChildren, FunctionComponent, SourceNode } from "#jsx/jsx-runtime"; +import { setImmediate } from "node:timers/promises"; +import { format } from "prettier"; +import { MetaNode, getMeta } from "./metatree.js"; +import { getPrinter } from "./use-printer.js"; +import { notifyResolved } from "./use-resolved.js"; + +export interface RenderContext { + node?: RenderedTreeNode; + meta?: MetaNode; +} + +let renderContext: RenderContext = { + node: undefined, + meta: undefined, +}; + +export function getRenderContext() { + return renderContext; +} + +export type RenderedTreeNode = (string | RenderedTreeNode)[]; + +const intrinsicMap: Record = { + rb: "}", + lb: "{", + br: "\n", +}; + +export interface SourceFileRecord { + path: string; + content: string; +} + +interface RenderToSourceFilesOptions { + format?: boolean; +} + +export async function renderToSourceFiles( + root: SourceNode, + options: RenderToSourceFilesOptions = { format: true } +): Promise { + const res = await render(root); + const sourceFiles: SourceFileRecord[] = []; + + for (const node of res) { + if (!Array.isArray(node)) { + continue; + } + + const files = await findRenderedSourceFiles(node); + sourceFiles.push(...files); + } + + return sourceFiles; +} + +/** + * Walks a tree of rendered nodes and returns all source files found. + * @param node a rendered node + * @returns a list of rendered source files. + */ +async function findRenderedSourceFiles(node: RenderedTreeNode): Promise { + const files: SourceFileRecord[] = []; + + // If the node is not an array, there are no source files to find. + if (!Array.isArray(node)) { + return files; + } + + // Walk the children of the node. + for (const child of node) { + // If the child is not an array, it is a string and not a source file. + if (!Array.isArray(child)) { + continue; + } + + // Get the meta data for the child node. With this we can figure out if + // it is a soruce file or not. + const meta = getMeta(child); + if (meta.sourceFile) { + // Extract metadata from the metatree for the rendered source file and add it to the list. + files.push({ + path: meta.sourceFile.path, + content: await printFormatted(child, meta.sourceFile.fileType), + }); + } else { + // Recursively find source files in the child node. + const childFiles = await findRenderedSourceFiles(child); + files.push(...childFiles); + } + } + + return files; +} + +export async function render(root: SourceNode): Promise { + // todo: check for forward progress. + // I /think/ this should work to ensure render doesn't resolve until + // all async work called from render finishes. But, it won't work + // for any async work that is queued as a result of an resolution. + + const res = renderWorker(root); + await setImmediate(); + await notifyResolved(); + + return res; +} + +export function renderWorker(root: SourceNode): RenderedTreeNode { + if (isIntrinsicComponent(root)) { + return [intrinsicMap[root.type]]; + } + + assertIsFunctionComponent(root); + const node: RenderedTreeNode = []; + const meta = getMeta(node); + meta.parent = renderContext.meta; + meta.type = root.type; + const oldContext = renderContext; + renderContext = { + meta, + node, + }; + + let children = root.type(root.props); + if (children instanceof Promise) { + children.then((children) => { + handleChildren(node, children); + }); + } else { + handleChildren(node, children); + } + + renderContext = oldContext; + + return node; +} + +function handleChildren(node: RenderedTreeNode, children: ComponentChildren) { + if (!Array.isArray(children)) { + children = [children]; + } + + children = children.flat(Infinity); + + for (const child of children) { + if (isSourceNode(child)) { + const childRender = renderWorker(child); + node.push(childRender); + } else if (child instanceof Promise) { + const index = node.push("{ pending }"); + child.then((v) => { + node[index - 1] = v; + }); + } else if (child === undefined || child === null || typeof child === "boolean") { + continue; + } else { + node.push(String(child)); + } + } +} + +function isIntrinsicComponent(node: SourceNode): node is SourceNode & { type: string } { + return typeof node.type === "string"; +} + +function assertIsFunctionComponent( + node: SourceNode +): asserts node is SourceNode & { type: FunctionComponent } { + if (typeof node.type !== "function") { + throw new Error("Expected function component"); + } +} + +function isSourceNode(element: ComponentChild): element is SourceNode { + return typeof element === "object" && element !== null && Object.hasOwn(element, "type"); +} + +export async function printFormatted( + root: RenderedTreeNode, + filetype: "typescript" | "python" +): Promise { + const raw = print(root); + // todo: handle langauges we can format/not format. + if (filetype === "typescript") { + return format(raw, { parser: filetype }); + } else { + return raw; + } +} + +export function print(root: RenderedTreeNode): string { + const customPrinter = getPrinter(root); + if (customPrinter) { + return customPrinter(root); + } + + return printChildren(root); +} + +export function printChildren(root: RenderedTreeNode): string { + let printed = ""; + for (const child of root) { + if (typeof child === "string") { + printed += child; + } else { + printed += print(child); + } + } + + return printed; +} diff --git a/packages/efnext/src/framework/core/use-printer.ts b/packages/efnext/src/framework/core/use-printer.ts new file mode 100644 index 00000000000..2d6cb316415 --- /dev/null +++ b/packages/efnext/src/framework/core/use-printer.ts @@ -0,0 +1,12 @@ +import { RenderedTreeNode, getRenderContext } from "./render.js"; + +const printers = new Map string>(); + +export function usePrinter(cb: (node: RenderedTreeNode) => string) { + const renderContext = getRenderContext(); + printers.set(renderContext.node!, cb); +} + +export function getPrinter(node: RenderedTreeNode) { + return printers.get(node); +} diff --git a/packages/efnext/src/framework/core/use-resolved.ts b/packages/efnext/src/framework/core/use-resolved.ts new file mode 100644 index 00000000000..8a2b3caaef8 --- /dev/null +++ b/packages/efnext/src/framework/core/use-resolved.ts @@ -0,0 +1,34 @@ +import { SourceNode } from "#jsx/jsx-runtime"; +import { RenderedTreeNode, getRenderContext, renderWorker } from "./render.js"; + +const resolveCallbacks = new Set<() => Promise>(); + +export function useResolved(cb: () => SourceNode) { + let nodeToFill: RenderedTreeNode; + + resolveCallbacks.add(async () => { + if (!nodeToFill) { + throw new Error("Didn't find place to put content"); + } + + const children = await renderWorker(cb()); + nodeToFill.push(children); + }); + + return function OnResolved() { + const renderContext = getRenderContext(); + if (!renderContext.node) { + throw new Error("Need node to put stuff in"); + } + + nodeToFill = renderContext.node; + return; + }; +} + +export async function notifyResolved() { + for (const cb of resolveCallbacks) { + await cb(); + } + resolveCallbacks.clear(); +} diff --git a/packages/efnext/src/framework/index.ts b/packages/efnext/src/framework/index.ts new file mode 100644 index 00000000000..cbeda51dc95 --- /dev/null +++ b/packages/efnext/src/framework/index.ts @@ -0,0 +1,3 @@ +export * from "./components/index.js"; +export * from "./core/index.js"; +export * from "./utils/index.js"; diff --git a/packages/efnext/src/framework/utils/children-component-utils.ts b/packages/efnext/src/framework/utils/children-component-utils.ts new file mode 100644 index 00000000000..2da7be86ed2 --- /dev/null +++ b/packages/efnext/src/framework/utils/children-component-utils.ts @@ -0,0 +1,25 @@ +// const bodyChild = coerceArray(children)?.find((child: any) => child.type === Function.Body); + +import { ComponentChild, ComponentChildren, isSourceNode } from "#jsx/jsx-runtime"; +import { coerceArray } from "./coerce-array.js"; + +/** + * Filters a list of children into two lists: one containing only children of a specific type, and the other containing all other children. + */ +export function filterComponentFromChildren( + children: ComponentChildren, + type: unknown +): [ComponentChild[], ComponentChild[] | undefined] { + const childrenArray = coerceArray(children); + const matches: ComponentChild[] = []; + const other: ComponentChild[] = []; + for (const child of childrenArray ?? []) { + if (isSourceNode(child) && child.type === type) { + matches.push(child); + } else { + other.push(child); + } + } + + return [matches, other]; +} diff --git a/packages/efnext/src/framework/utils/coerce-array.ts b/packages/efnext/src/framework/utils/coerce-array.ts new file mode 100644 index 00000000000..b0f4e6c15d9 --- /dev/null +++ b/packages/efnext/src/framework/utils/coerce-array.ts @@ -0,0 +1,7 @@ +export function coerceArray(v: unknown): any { + if (v === null || v === undefined || Array.isArray(v)) { + return v; + } + + return [v]; +} diff --git a/packages/efnext/src/framework/utils/index.ts b/packages/efnext/src/framework/utils/index.ts new file mode 100644 index 00000000000..2ee5727d086 --- /dev/null +++ b/packages/efnext/src/framework/utils/index.ts @@ -0,0 +1,3 @@ +export * from "./children-component-utils.js"; +export * from "./coerce-array.js"; +export * from "./typeguards.js"; diff --git a/packages/efnext/src/framework/utils/typeguards.ts b/packages/efnext/src/framework/utils/typeguards.ts new file mode 100644 index 00000000000..3f935f1b64a --- /dev/null +++ b/packages/efnext/src/framework/utils/typeguards.ts @@ -0,0 +1,77 @@ +import { + ArrayModelType, + Enum, + Interface, + IntrinsicType, + Model, + ModelProperty, + Namespace, + Operation, + RecordModelType, + Scalar, + Type, + Union, +} from "@typespec/compiler"; + +export function isModel(type: any): type is Model { + return type.kind === "Model"; +} + +export function isInterface(type: any): type is Interface { + return type.kind === "Interface"; +} + +export function isOperation(type: any): type is Operation { + return type.kind === "Operation"; +} + +export function isModelProperty(type: any): type is ModelProperty { + return type.kind === "ModelProperty"; +} + +export function isScalar(type: any): type is Scalar { + return type.kind === "Scalar"; +} + +export function isIntrinsic(type: any): type is IntrinsicType { + return type.kind === "Intrinsic"; +} + +export function isArray(type: any): type is ArrayModelType { + return type.name === "Array" && Boolean(type.indexer); +} + +export function isRecord(type: any): type is RecordModelType { + return type.name === "Record" && Boolean(type.indexer); +} + +export type TypeSpecDeclaration = + | Model + | Interface + | Union + | Operation + | Enum + | Scalar + | IntrinsicType; + +/** + * Returns true if the given type is a declaration or an instantiation of a declaration. + * @param type + * @returns + */ +export function isDeclaration(type: Type): type is TypeSpecDeclaration | Namespace { + switch (type.kind) { + case "Namespace": + case "Interface": + case "Enum": + case "Operation": + return true; + + case "Model": + return type.name ? type.name !== "" && type.name !== "Array" : false; + case "Union": + return type.name ? type.name !== "" : false; + default: + return false; + } +} diff --git a/packages/efnext/src/index.tsx b/packages/efnext/src/index.tsx new file mode 100644 index 00000000000..a6c1fdf4511 --- /dev/null +++ b/packages/efnext/src/index.tsx @@ -0,0 +1,7 @@ +import { EmitContext } from "@typespec/compiler"; + +export async function $onEmit(context: EmitContext) { + if (context.program.compilerOptions.noEmit) { + return; + } +} diff --git a/packages/efnext/src/python/array-expression.tsx b/packages/efnext/src/python/array-expression.tsx new file mode 100644 index 00000000000..14c2ce3db30 --- /dev/null +++ b/packages/efnext/src/python/array-expression.tsx @@ -0,0 +1,17 @@ +import { Type } from "@typespec/compiler"; +import { TypeExpression } from "./type-expression.js"; +import { stdlib } from "./builtins.js"; +import { Reference } from "./reference.js"; + +export interface ArrayExpressionProps { + elementType: Type; +} + +export function ArrayExpression({ elementType }: ArrayExpressionProps) { + return ( + <> + [ + ] + + ); +} diff --git a/packages/efnext/src/python/builtins.ts b/packages/efnext/src/python/builtins.ts new file mode 100644 index 00000000000..a3071d29103 --- /dev/null +++ b/packages/efnext/src/python/builtins.ts @@ -0,0 +1,11 @@ +export const stdlib = { + typing: { + List: ["typings", "List"], + Union: ["typings", "Union"], + Literal: ["typings", "Literal"], + Tuple: ["typings", "Tuple"], + }, + enum: { + Enum: ["enum", "Enum"], + }, +} as const; diff --git a/packages/efnext/src/python/class-declaration.tsx b/packages/efnext/src/python/class-declaration.tsx new file mode 100644 index 00000000000..7a3f5b2da45 --- /dev/null +++ b/packages/efnext/src/python/class-declaration.tsx @@ -0,0 +1,31 @@ +import { ComponentChildren } from "#jsx/jsx-runtime"; +import { Model } from "@typespec/compiler"; +import { Declaration } from "../framework/components/declaration.js"; +import { code } from "../framework/core/code.js"; +import { useNamePolicy } from "../framework/core/name-policy.js"; +import { mapWithSep } from "./utils.js"; +import { ClassMember } from "./class-member.js"; + +export interface ClassDeclarationProps { + type: Model; + name?: string; + children?: ComponentChildren; +} + +export function ClassDeclaration({ type, name, children }: ClassDeclarationProps) { + const namer = useNamePolicy(); + const className = name ?? namer.getName(type, "class"); + const members = mapWithSep(type.properties.values(), (prop) => { + return + }, "\n") + + return ( + + {code` + class ${className}: + ${members} + ${children} + `} + + ); +} diff --git a/packages/efnext/src/python/class-member.tsx b/packages/efnext/src/python/class-member.tsx new file mode 100644 index 00000000000..d3390a937a3 --- /dev/null +++ b/packages/efnext/src/python/class-member.tsx @@ -0,0 +1,16 @@ +import { ModelProperty } from "@typespec/compiler"; +import { TypeExpression } from "./type-expression.js"; +import { useNamePolicy } from "../framework/core/name-policy.js"; + +export interface ClassMemberProps { + type: ModelProperty; +} + +export function ClassMember({ type }: ClassMemberProps) { + const name = useNamePolicy().getName(type, "classMember"); + return ( + <> + {name}: + + ); +} diff --git a/packages/efnext/src/python/dictionary-expression.tsx b/packages/efnext/src/python/dictionary-expression.tsx new file mode 100644 index 00000000000..242c31baff4 --- /dev/null +++ b/packages/efnext/src/python/dictionary-expression.tsx @@ -0,0 +1,14 @@ +import { Type } from "@typespec/compiler"; +import { TypeExpression } from "./type-expression.js"; + +export interface DictionaryExpressionProps { + elementType: Type; +} + +export function DictionaryExpression({ elementType }: DictionaryExpressionProps) { + return ( + <> + Dict[string, ] + + ); +} diff --git a/packages/efnext/src/python/enum-declaration.tsx b/packages/efnext/src/python/enum-declaration.tsx new file mode 100644 index 00000000000..ce7eb7858ce --- /dev/null +++ b/packages/efnext/src/python/enum-declaration.tsx @@ -0,0 +1,36 @@ +import { ComponentChildren } from "#jsx/jsx-runtime"; +import { Enum } from "@typespec/compiler"; +import { Declaration } from "../framework/components/declaration.js"; +import { code } from "../framework/core/code.js"; +import { mapWithSep } from "./utils.js"; +import { useNamePolicy } from "../framework/core/name-policy.js"; +import { Reference } from "./reference.js"; +import { stdlib } from "./builtins.js"; + +export interface EnumDeclarationProps { + type: Enum; + children?: ComponentChildren; +} + +export function EnumDeclaration({ type, children }: EnumDeclarationProps) { + const namer = useNamePolicy(); + + const members = mapWithSep(type.members.values(), (value) => { + if (value.value) { + // todo: probably delegate to a value-producing component + return `${namer.getName(value, "enumMember")} = ${JSON.stringify(value)}`; + } else { + return `${namer.getName(value, "enumMember")} = "${value.name}"` + } + }, "\n"); + + return ( + + {code` + class ${type.name}(${}): + ${members} + ${children} + `} + + ); +} diff --git a/packages/efnext/src/python/function.tsx b/packages/efnext/src/python/function.tsx new file mode 100644 index 00000000000..4457453d848 --- /dev/null +++ b/packages/efnext/src/python/function.tsx @@ -0,0 +1,79 @@ +import { SourceNode } from "#jsx/jsx-runtime"; +import { Model, Operation } from "@typespec/compiler"; +import { Declaration } from "../framework/components/declaration.js"; +import { code } from "../framework/core/code.js"; +import { useNamePolicy } from "../framework/core/name-policy.js"; +import { coerceArray } from "../framework/utils/coerce-array.js"; +import { TypeExpression } from "./type-expression.js"; + +export interface FunctionProps { + type?: Operation; + name?: string; + refkey?: unknown; + children?: SourceNode[]; +} + +export function Function({ type, name, children, refkey }: FunctionProps) { + const functionName = name ?? useNamePolicy().getName(type!, "function"); + const parameters = type?.parameters; + const parametersChild = coerceArray(children)?.find( + (child: any) => child.type === Function.Parameters + ); + const bodyChild = coerceArray(children)?.find((child: any) => child.type === Function.Body); + const sReturnType = type?.returnType ? : undefined; + const sParams = parametersChild ? ( + parametersChild + ) : ( + + ); + + let sBody = bodyChild ? bodyChild : {children}; + + return ( + + {code` + def ${functionName}(${sParams}) -> ${sReturnType}: + ${sBody} + `} + + ); +} + +export interface FunctionParametersProps { + parameters?: Model; + children?: SourceNode[]; +} + +// todo: update to be inline with typescript framework +Function.Parameters = function Parameters({ parameters, children }: FunctionParametersProps) { + const namer = useNamePolicy(); + if (children) { + return children; + } else { + const params = Array.from(parameters?.properties.values() ?? []); + return ( + <> + {params.map((param, index) => { + const isLast = index === params.length - 1; + const optionality = param.optional ? "?" : ""; + return ( + <> + {namer.getName(param, "parameter")} + {optionality}: + {!isLast ? ", " : ""} + + ); + })} + + ); + } +}; + +export interface FunctionBodyProps { + operation?: Operation; + children?: SourceNode[]; +} + +Function.Body = function Body({ operation, children }: FunctionBodyProps) { + return children; +}; diff --git a/packages/efnext/src/python/index.ts b/packages/efnext/src/python/index.ts new file mode 100644 index 00000000000..0f8e2c47168 --- /dev/null +++ b/packages/efnext/src/python/index.ts @@ -0,0 +1,14 @@ +export * from "./array-expression.js"; +export * from "./builtins.js"; +export * from "./class-declaration.js"; +export * from "./class-member.js"; +export * from "./dictionary-expression.js"; +export * from "./enum-declaration.js"; +export * from "./function.js"; +export * from "./naming-policy.js"; +export * from "./reference.js"; +export * from "./type-declaration.js"; +export * from "./type-expression.js"; +export * from "./type-literal.js"; +export * from "./union-declaration.js"; +export * from "./union-expression.js"; diff --git a/packages/efnext/src/python/naming-policy.ts b/packages/efnext/src/python/naming-policy.ts new file mode 100644 index 00000000000..b7be90e3b70 --- /dev/null +++ b/packages/efnext/src/python/naming-policy.ts @@ -0,0 +1,70 @@ +import { isTemplateInstance } from "@typespec/compiler"; +import { pascalCase, snakeCase } from "change-case"; +import { createNamePolicy } from "../framework/core/name-policy.js"; +import { isDeclaration } from "../framework/utils/typeguards.js"; + +// todo: provide in Python framework + +type NameKinds = "classMember" | "class" | "function" | "parameter" | "enumMember"; + +export const pythonNamePolicy = createNamePolicy((type, kind) => { + if (!("name" in type && typeof type.name === "string")) { + return "unknown name"; + } + + let name; + if (isTemplateInstance(type) && isDeclaration(type)) { + let unspeakable = false; + + const parameterNames = type.templateMapper.args.map((t) => { + if (t.entityKind === "Indeterminate") { + t = t.type; + } + if (!("kind" in t)) { + return undefined; + } + switch (t.kind) { + case "Model": + case "Scalar": + case "Interface": + case "Operation": + case "Enum": + case "Union": + case "Intrinsic": + if (!t.name) { + unspeakable = true; + return undefined; + } + const declName: string = pythonNamePolicy.renamer.getName(t, "class"); + if (declName === undefined) { + unspeakable = true; + return undefined; + } + return declName[0].toUpperCase() + declName.slice(1); + default: + unspeakable = true; + return undefined; + } + }); + + if (unspeakable) { + return "Unspeakable"; + } + + name = type.name + parameterNames.join(""); + } else { + name = type.name; + } + + switch (kind) { + case "class": + return pascalCase(name); + case "classMember": + case "function": + case "parameter": + case "enumMember": + return snakeCase(name); + default: + throw new Error(`Unknown kind ${kind}`); + } +}); diff --git a/packages/efnext/src/python/reference.tsx b/packages/efnext/src/python/reference.tsx new file mode 100644 index 00000000000..85a3fa1be57 --- /dev/null +++ b/packages/efnext/src/python/reference.tsx @@ -0,0 +1,35 @@ +import { ScopeContext } from "../framework/components/scope.js"; +import { SourceFileContext } from "../framework/components/source-file.js"; +import { useContext } from "../framework/core/context.js"; + +export interface ReferenceProps { + refkey?: unknown; + builtin?: readonly [string, string] | [string, string]; +} +export async function Reference({ refkey, builtin }: ReferenceProps) { + const sourceFile = useContext(SourceFileContext); + + if (builtin) { + sourceFile!.addImport({ importPath: builtin[0], name: builtin[1]}); + return builtin[1]; + } + + const scope = useContext(ScopeContext); + if (!scope) { + throw new Error("Need scope context to form references"); + } + const binder = scope.binder; + + const result = await binder.resolveOrWaitForDeclaration(scope, refkey); + if (!result.resolved) { + throw new Error("Failed to resolve"); + } + + const { targetDeclaration, pathDown } = result; + if (pathDown.length > 0 && pathDown[0].kind === "module") { + // TODO update import + sourceFile!.addImport({ importPath: pathDown[0].path, name: targetDeclaration.name }); + } + + return targetDeclaration.name; +} diff --git a/packages/efnext/src/python/type-declaration.tsx b/packages/efnext/src/python/type-declaration.tsx new file mode 100644 index 00000000000..839851885c0 --- /dev/null +++ b/packages/efnext/src/python/type-declaration.tsx @@ -0,0 +1,21 @@ +import { Type } from "@typespec/compiler"; +import { ClassDeclaration } from "./class-declaration.js"; +import { UnionDeclaration } from "./union-declaration.js"; +import { EnumDeclaration } from "./enum-declaration.js"; + +export interface TypeDeclarationProps { + type: Type; +} + +export function TypeDeclaration({ type }: TypeDeclarationProps) { + switch (type.kind) { + case "Model": + return ; + case "Union": + return ; + case "Enum": + return ; + default: + throw new Error("Not yet supported"); + } +} diff --git a/packages/efnext/src/python/type-expression.tsx b/packages/efnext/src/python/type-expression.tsx new file mode 100644 index 00000000000..17d9464969c --- /dev/null +++ b/packages/efnext/src/python/type-expression.tsx @@ -0,0 +1,117 @@ +import { ComponentChildren } from "#jsx/jsx-runtime"; +import { IntrinsicType, Model, Scalar, Type } from "@typespec/compiler"; +import { code } from "../framework/core/code.js"; +import { isArray, isDeclaration, isRecord } from "../framework/utils/typeguards.js"; +import { ArrayExpression } from "./array-expression.js"; +import { stdlib } from "./builtins.js"; +import { ClassMember } from "./class-member.js"; +import { DictionaryExpression } from "./dictionary-expression.js"; +import { Reference } from "./reference.js"; +import { TypeLiteral } from "./type-literal.js"; +import { UnionExpression } from "./union-expression.js"; +import { mapWithSep } from "./utils.js"; + +export interface TypeExpressionProps { + type: Type; + children?: ComponentChildren; +} + +export function TypeExpression({ type, children }: TypeExpressionProps) { + if (isDeclaration(type) && !(type as Model).indexer) { + // todo: probably need abstraction around deciding what's a declaration in the output + // (it may not correspond to things which are declarations in TypeSpec?) + return ; + } + + switch (type.kind) { + case "Scalar": + case "Intrinsic": + return getScalarIntrinsicExpression(type); + case "Boolean": + case "Number": + case "String": + return ; + case "Union": + return ; + case "Tuple": + return ( + <> + [ + {type.values.map((element) => ( + <> + , + + ))} + ] + + ); + case "EnumMember": + return ( + <> + [{type.enum.name}.{type.name}.value] + + ); + case "Model": + if (isArray(type)) { + const elementType = type.indexer.value; + return ; + } + + if (isRecord(type)) { + const elementType = type.indexer.value; + return ; + } + + const members = mapWithSep( + type.properties.values(), + (prop) => { + return ; + }, + "\n" + ); + + return code` + ${members} + ${children} + `; + + default: + throw new Error(type.kind + " not supported in TypeExpression"); + } +} + +const intrinsicNameToPythonType = new Map([ + ["unknown", "Any"], + ["string", "str"], + ["int32", "int"], + ["int16", "int"], + ["float16", "float"], + ["integer", "int"], + ["float", "float"], + ["float32", "float"], + ["int64", "int"], // Python's int can handle arbitrarily large integers + ["boolean", "bool"], + ["null", "None"], + ["void", "None"], + ["numeric", "float"], // Alternatively, "Union[int, float]" if mixed types are common + ["uint64", "int"], // Python's int can handle unsigned 64-bit integers + ["uint32", "int"], + ["uint16", "int"], + ["bytes", "bytes"], + ["float64", "float"], + ["safeint", "int"], + ["utcDateTime", "datetime.datetime"], + ["url", "str"], +]); + +function getScalarIntrinsicExpression(type: Scalar | IntrinsicType): string { + if (type.kind === "Scalar" && type.baseScalar && type.namespace?.name !== "TypeSpec") { + // This is a delcared scalar + return ; + } + const pythonType = intrinsicNameToPythonType.get(type.name); + if (!pythonType) { + throw new Error(`Unknown scalar type ${type.name}`); + } + return pythonType; +} diff --git a/packages/efnext/src/python/type-literal.tsx b/packages/efnext/src/python/type-literal.tsx new file mode 100644 index 00000000000..797b0059687 --- /dev/null +++ b/packages/efnext/src/python/type-literal.tsx @@ -0,0 +1,15 @@ +import { BooleanLiteral, NumericLiteral, StringLiteral } from "@typespec/compiler"; + +export interface TypeLiteralProps { + type: BooleanLiteral | StringLiteral | NumericLiteral; +} + +export function TypeLiteral({ type }: TypeLiteralProps) { + switch (type.kind) { + case "Boolean": + case "Number": + return `Literal[${String(type.value)}]`; + case "String": + return `Literal["${type.value}"]`; + } +} diff --git a/packages/efnext/src/python/union-declaration.tsx b/packages/efnext/src/python/union-declaration.tsx new file mode 100644 index 00000000000..20a72c80832 --- /dev/null +++ b/packages/efnext/src/python/union-declaration.tsx @@ -0,0 +1,15 @@ +import { Union } from "@typespec/compiler"; +import { Declaration } from "../framework/components/declaration.js"; +import { UnionExpression } from "./union-expression.js"; + +export interface UnionDeclarationProps { + type: Union; +} + +export function UnionDeclaration({ type }: UnionDeclarationProps) { + return ( + + {type.name} = + + ); +} diff --git a/packages/efnext/src/python/union-expression.tsx b/packages/efnext/src/python/union-expression.tsx new file mode 100644 index 00000000000..342f42354eb --- /dev/null +++ b/packages/efnext/src/python/union-expression.tsx @@ -0,0 +1,17 @@ +import { Union } from "@typespec/compiler"; +import { stdlib } from "./builtins.js"; +import { Reference } from "./reference.js"; +import { TypeExpression } from "./type-expression.js"; +import { mapWithSep, withSep } from "./utils.js"; + +export interface UnionExpressionProps { + type: Union; +} + +export function UnionExpression({ type }: UnionExpressionProps) { + const children = mapWithSep(type.variants.values(), (variant) => { + return ; + }); + + return <>[{children}]; +} diff --git a/packages/efnext/src/python/utils.ts b/packages/efnext/src/python/utils.ts new file mode 100644 index 00000000000..b287bcdf686 --- /dev/null +++ b/packages/efnext/src/python/utils.ts @@ -0,0 +1,34 @@ +import { ComponentChild, ComponentChildren } from "#jsx/jsx-runtime"; + +export function* withSep( + iterator: Iterable, + joiner = ", " +): Generator<[T, string | undefined]> { + const iter = iterator[Symbol.iterator](); + let current = iter.next(); + let next = iter.next(); + + while (!current.done) { + yield [current.value, next.done ? undefined : joiner]; + current = next; + next = iter.next(); + } +} + +export function mapWithSep( + iterator: Iterable, + mapFn: (value: T) => ComponentChild, + joiner = ", " +): ComponentChildren { + const children: ComponentChildren = []; + + const separated = withSep(iterator, joiner); + for (const [value, separator] of separated) { + children.push(mapFn(value)); + if (separator) { + children.push(separator); + } + } + + return children; +} diff --git a/packages/efnext/src/testing/index.ts b/packages/efnext/src/testing/index.ts new file mode 100644 index 00000000000..453a3317122 --- /dev/null +++ b/packages/efnext/src/testing/index.ts @@ -0,0 +1,8 @@ +import { resolvePath } from "@typespec/compiler"; +import { createTestLibrary, TypeSpecTestLibrary } from "@typespec/compiler/testing"; +import { fileURLToPath } from "url"; + +export const TestLibrary: TypeSpecTestLibrary = createTestLibrary({ + name: "@typespec/efnext", + packageRoot: resolvePath(fileURLToPath(import.meta.url), "../../../"), +}); diff --git a/packages/efnext/src/types/index.d.ts b/packages/efnext/src/types/index.d.ts new file mode 100644 index 00000000000..1289c29b32b --- /dev/null +++ b/packages/efnext/src/types/index.d.ts @@ -0,0 +1,7 @@ +declare namespace JSX { + interface IntrinsicElements { + lb: object; + rb: object; + br: object; + } +} diff --git a/packages/efnext/src/typescript-interface-emitter.tsx b/packages/efnext/src/typescript-interface-emitter.tsx new file mode 100644 index 00000000000..2df9a657cd5 --- /dev/null +++ b/packages/efnext/src/typescript-interface-emitter.tsx @@ -0,0 +1,73 @@ +import { EmitContext, Program } from "@typespec/compiler"; +import { dirname, join } from "path"; +import { format } from "prettier"; +import { EmitOutput, SourceFile } from "../src/framework/components/index.js"; +import { EnumDeclaration } from "../src/typescript/enum-declaration.js"; +import { FunctionDeclaration } from "../src/typescript/function-declaration.js"; +import { InterfaceDeclaration } from "../src/typescript/interface-declaration.js"; +import { ScalarDeclaration } from "../src/typescript/scalar-declaration.js"; +import { UnionDeclaration } from "../src/typescript/union-declaration.js"; +import { SourceDirectory } from "./framework/components/source-directory.js"; +import { renderToSourceFiles } from "./framework/core/render.js"; + +export async function $onEmit(context: EmitContext) { + const rendered = await renderToSourceFiles(emitTypescriptInterfaces(context.program)); + for (const file of rendered) { + const path = join(context.emitterOutputDir, file.path); + await context.program.host.mkdirp(dirname(path)); + const output = await format(file.content, { parser: "typescript" }); + await context.program.host.writeFile(path, output); + } +} + +export function emitTypescriptInterfaces(program: Program) { + const globalNamespace = program.getGlobalNamespaceType(); + + const namespaces = [...globalNamespace.namespaces.values()].filter( + (ns) => ns.name !== "TypeSpec" + ); + + namespaces.unshift(globalNamespace); + + return ( + + {namespaces.map((namespace) => { + const models = [...namespace.models.values()]; + const operations = [...namespace.operations.values()]; + const enums = [...namespace.enums.values()]; + const unions = [...namespace.unions.values()]; + const interfaces = [...namespace.interfaces.values()]; + const scalars = [...namespace.scalars.values()]; + + const file = ( + + {models.map((model) => ( + + ))} + {operations.map((operation) => ( + + ))} + {enums.map((enumType) => ( + + ))} + {unions.map((union) => ( + + ))} + {interfaces.map((iface) => ( + + ))} + {scalars.map((scalar) => ( + + ))} + + ); + + if (namespace.name) { + return {file}; + } + + return file; + })} + + ); +} diff --git a/packages/efnext/src/typescript/array-expression.tsx b/packages/efnext/src/typescript/array-expression.tsx new file mode 100644 index 00000000000..48a2e69ab67 --- /dev/null +++ b/packages/efnext/src/typescript/array-expression.tsx @@ -0,0 +1,14 @@ +import { Type } from "@typespec/compiler"; +import { TypeExpression } from "./type-expression.js"; + +export interface ArrayExpressionProps { + elementType: Type; +} + +export function ArrayExpression({ elementType }: ArrayExpressionProps) { + return ( + <> + () [] + + ); +} diff --git a/packages/efnext/src/typescript/block.tsx b/packages/efnext/src/typescript/block.tsx new file mode 100644 index 00000000000..d4cc7cba0ab --- /dev/null +++ b/packages/efnext/src/typescript/block.tsx @@ -0,0 +1,10 @@ +export function Block({ children }: any) { + return ( + <> + + {children} + +
+ + ); +} diff --git a/packages/efnext/src/typescript/enum-declaration.tsx b/packages/efnext/src/typescript/enum-declaration.tsx new file mode 100644 index 00000000000..c24ea6a0d28 --- /dev/null +++ b/packages/efnext/src/typescript/enum-declaration.tsx @@ -0,0 +1,26 @@ +import { ComponentChildren } from "#jsx/jsx-runtime"; +import { Enum } from "@typespec/compiler"; +import { Declaration } from "../framework/components/declaration.js"; +import { Block } from "./block.js"; +import { EnumExpression } from "./enum-expression.js"; + +export interface EnumDeclarationProps { + type: Enum; + children?: ComponentChildren; +} + +export function EnumDeclaration({ type, children }: EnumDeclarationProps) { + const members = [...type.members.values()]; + + return ( + + export enum {type.name}{" "} + + {members.map((member) => ( + + ))} + {children} + + + ); +} diff --git a/packages/efnext/src/typescript/enum-expression.tsx b/packages/efnext/src/typescript/enum-expression.tsx new file mode 100644 index 00000000000..228ff34503c --- /dev/null +++ b/packages/efnext/src/typescript/enum-expression.tsx @@ -0,0 +1,16 @@ +import { EnumMember } from "@typespec/compiler"; + +export interface EnumExpressionProps { + type: EnumMember; +} + +export function EnumExpression({ type }: EnumExpressionProps) { + const value = type.value ? ` = ${JSON.stringify(type.value)}` : ""; + + return ( + <> + {type.name} + {value}, + + ); +} diff --git a/packages/efnext/src/typescript/function-declaration.tsx b/packages/efnext/src/typescript/function-declaration.tsx new file mode 100644 index 00000000000..49a6becc070 --- /dev/null +++ b/packages/efnext/src/typescript/function-declaration.tsx @@ -0,0 +1,141 @@ +import { ComponentChildren, SourceNode } from "#jsx/jsx-runtime"; +import { Model, ModelProperty, Operation, Union } from "@typespec/compiler"; +import { Declaration } from "../framework/components/declaration.js"; +import { code } from "../framework/core/code.js"; +import { useNamePolicy } from "../framework/core/name-policy.js"; +import { filterComponentFromChildren } from "../framework/utils/children-component-utils.js"; +import { Block } from "./block.js"; +import { TypeExpression } from "./type-expression.js"; + +export interface FunctionDeclarationProps { + type?: Operation; + refkey?: unknown; + name?: string; + parameters?: Record; + children?: ComponentChildren; +} + +export function FunctionDeclaration({ + type, + parameters, + name, + refkey, + children: allChildren, +}: FunctionDeclarationProps) { + const functionName = name ?? useNamePolicy().getName(type!, "function"); + + const [parametersChild, childrenWithNoParams] = filterComponentFromChildren( + allChildren, + FunctionDeclaration.Parameters + ); + const [bodyChild, childrenWithNoBody] = filterComponentFromChildren( + childrenWithNoParams, + FunctionDeclaration.Body + ); + const [customReturnType, children] = filterComponentFromChildren( + childrenWithNoBody, + FunctionDeclaration.ReturnType + ); + + let sReturnType = type?.returnType ? ( + <> + : + + ) : undefined; + + if (customReturnType?.length) { + sReturnType = <>: {customReturnType}; + } + + const sParams = parametersChild?.length ? ( + parametersChild + ) : ( + + ); + + const sBody = bodyChild?.length ? ( + bodyChild + ) : ( + {children} + ); + + return ( + + export function {functionName} ({sParams}) {sReturnType} + {sBody} + + ); +} + +export interface FunctionParametersProps { + type?: Model; + parameters?: Record | ModelProperty[]; + children?: SourceNode[]; +} + +FunctionDeclaration.Parameters = function Parameters({ + type, + parameters, + children, +}: FunctionParametersProps) { + if (children) { + return children; + } else if (parameters) { + if (Array.isArray(parameters)) { + return buildParameters(parameters); + } + return Object.entries(parameters).map(([key, value]) => [key, ":", value, ","]); + } else { + const params = Array.from(type?.properties.values() ?? []); + return {buildParameters(params)}; + } +}; + +function buildParameters(params: ModelProperty[]): SourceNode[] { + return params.map((param, index) => { + const isLast = index === params.length - 1; + const optionality = param.optional ? "?" : ""; + return ( + <> + {param.name} + {optionality}: + {!isLast ? ", " : ""} + + ); + }); +} + +function WrappingParameter({ type, children }: { type?: Model; children?: ComponentChildren }) { + if (!type || !type.name) { + return <>{children}; + } + + const paramName = type.name; // TODO: rename + return code` + ${paramName}: {${children}} + `; +} + +export interface FunctionBodyProps { + operation?: Operation; + children?: SourceNode[]; +} + +FunctionDeclaration.Body = function Body({ operation, children }: FunctionBodyProps) { + return children; +}; + +export interface FunctionReturnTypeProps { + type?: Model | Union; + children?: SourceNode[]; +} + +FunctionDeclaration.ReturnType = function ReturnType({ type, children }: FunctionReturnTypeProps) { + if (children) { + return children; + } else if (type) { + return ; + } + + return <>; +}; diff --git a/packages/efnext/src/typescript/index.ts b/packages/efnext/src/typescript/index.ts new file mode 100644 index 00000000000..78560e32872 --- /dev/null +++ b/packages/efnext/src/typescript/index.ts @@ -0,0 +1,19 @@ +export * from "./array-expression.js"; +export * from "./block.js"; +export * from "./enum-declaration.js"; +export * from "./enum-expression.js"; +export * from "./function-declaration.js"; +export * from "./interface-declaration.js"; +export * from "./interface-expression.js"; +export * from "./interface-member.js"; +export * from "./intersection-declaration.js"; +export * from "./naming-policy.js"; +export * from "./record-expression.js"; +export * from "./reference.js"; +export * from "./scalar-declaration.js"; +export * from "./type-declaration.js"; +export * from "./type-expression.js"; +export * from "./type-literal.js"; +export * from "./union-declaration.js"; +export * from "./union-expression.js"; +export * from "./value.js"; diff --git a/packages/efnext/src/typescript/interface-declaration.tsx b/packages/efnext/src/typescript/interface-declaration.tsx new file mode 100644 index 00000000000..d09a5105290 --- /dev/null +++ b/packages/efnext/src/typescript/interface-declaration.tsx @@ -0,0 +1,39 @@ +import { ComponentChildren } from "#jsx/jsx-runtime"; +import { Interface, Model } from "@typespec/compiler"; +import { Declaration } from "../framework/components/declaration.js"; +import { useNamePolicy } from "../framework/core/name-policy.js"; +import { isModel } from "../framework/utils/typeguards.js"; +import { InterfaceExpression } from "./interface-expression.js"; +import { Reference } from "./reference.js"; + +export interface InterfaceDeclarationProps { + type?: Model | Interface; + name?: string; + children?: ComponentChildren; +} + +export function InterfaceDeclaration({ type, name, children }: InterfaceDeclarationProps) { + let extendsClause = undefined; + + const namer = useNamePolicy(); + let ifaceName = name ?? "___NoName___"; + + if (!name && type) { + ifaceName = namer.getName(type, "interface"); + } + + if (type && isModel(type) && type.baseModel) { + extendsClause = ( + <> + extends + + ); + } + + return ( + + export interface {ifaceName} {extendsClause}{" "} + {children} + + ); +} diff --git a/packages/efnext/src/typescript/interface-expression.tsx b/packages/efnext/src/typescript/interface-expression.tsx new file mode 100644 index 00000000000..43e053b8107 --- /dev/null +++ b/packages/efnext/src/typescript/interface-expression.tsx @@ -0,0 +1,40 @@ +import { ComponentChildren } from "#jsx/jsx-runtime"; +import { Interface, Model, ModelProperty, Operation } from "@typespec/compiler"; +import { filterComponentFromChildren } from "../framework/utils/children-component-utils.js"; +import { isInterface, isModel } from "../framework/utils/typeguards.js"; +import { Block } from "./block.js"; +import { InterfaceMember } from "./interface-member.js"; + +export interface InterfaceExpressionProps { + type?: Model | Interface; + children?: ComponentChildren; +} + +export function InterfaceExpression({ type, children: allChildren }: InterfaceExpressionProps) { + const members = []; + let typeMembers: IterableIterator | undefined; + const [childrenMembers, children] = filterComponentFromChildren(allChildren, InterfaceMember); + + if (type) { + if (isModel(type)) { + typeMembers = type.properties.values(); + } else if (isInterface(type)) { + typeMembers = type.operations.values(); + } + + for (const prop of typeMembers ?? []) { + members.push(); + } + } + + if (childrenMembers) { + members.push(childrenMembers); + } + + return ( + + {members} + {children} + + ); +} diff --git a/packages/efnext/src/typescript/interface-member.tsx b/packages/efnext/src/typescript/interface-member.tsx new file mode 100644 index 00000000000..576cd0870e1 --- /dev/null +++ b/packages/efnext/src/typescript/interface-member.tsx @@ -0,0 +1,44 @@ +import { ComponentChildren } from "#jsx/jsx-runtime"; +import { ModelProperty, Operation } from "@typespec/compiler"; +import { useNamePolicy } from "../framework/core/name-policy.js"; +import { filterComponentFromChildren } from "../framework/utils/children-component-utils.js"; +import { isModelProperty, isOperation } from "../framework/utils/typeguards.js"; +import { FunctionDeclaration } from "./function-declaration.js"; +import { TypeExpression } from "./type-expression.js"; + +export interface InterfaceMemberProps { + type: ModelProperty | Operation; + children?: ComponentChildren; +} + +export function InterfaceMember({ type, children: allChildren }: InterfaceMemberProps) { + const namer = useNamePolicy(); + const name = namer.getName(type, "interfaceMember"); + if (isModelProperty(type)) { + return ( + <> + "{name}"{type.optional && "?"}: ; + + ); + } + + if (isOperation(type)) { + const [customReturnType] = filterComponentFromChildren( + allChildren, + FunctionDeclaration.ReturnType + ); + + const returnType = customReturnType.length ? ( + <>{customReturnType} + ) : ( + + ); + + return ( + <> + {name}( + ): {returnType}; + + ); + } +} diff --git a/packages/efnext/src/typescript/intersection-declaration.tsx b/packages/efnext/src/typescript/intersection-declaration.tsx new file mode 100644 index 00000000000..6928dd299b7 --- /dev/null +++ b/packages/efnext/src/typescript/intersection-declaration.tsx @@ -0,0 +1,62 @@ +import { ComponentChildren } from "#jsx/jsx-runtime"; +import { Model } from "@typespec/compiler"; +import { Declaration } from "../framework/components/declaration.js"; +import { code } from "../framework/core/code.js"; +import { useNamePolicy } from "../framework/core/name-policy.js"; +import { filterComponentFromChildren } from "../framework/utils/children-component-utils.js"; +import { InterfaceExpression } from "./interface-expression.js"; +import { Reference } from "./reference.js"; + +export interface IntersectionDeclarationProps { + type?: Model; + name?: string; + children?: ComponentChildren; +} + +export function IntersectionDeclaration({ + type, + name, + children: allChildren, +}: IntersectionDeclarationProps) { + const namer = useNamePolicy(); + let intersectionName = name ?? "___NoName___"; + + if (!name && type) { + intersectionName = namer.getName(type, "interface"); + } + + const [constituents, children] = filterComponentFromChildren( + allChildren, + IntersectionConstituent + ); + + if (type?.baseModel) { + constituents.push(); + } + + return ( + + export type {intersectionName} = + {constituents.map((constituent) => { + return code` + ${constituent} &`; + })} + {type ? {children} : <>{children}} + + ); +} + +export interface IntersectionConstituentProps { + type?: Model; + children?: ComponentChildren; +} + +export function IntersectionConstituent({ type, children }: IntersectionConstituentProps) { + const typeReference = type ? : <>; + + return ( + <> + {typeReference} {children} + + ); +} diff --git a/packages/efnext/src/typescript/naming-policy.ts b/packages/efnext/src/typescript/naming-policy.ts new file mode 100644 index 00000000000..5536a35e91d --- /dev/null +++ b/packages/efnext/src/typescript/naming-policy.ts @@ -0,0 +1,67 @@ +import { isTemplateInstance } from "@typespec/compiler"; +import { camelCase, pascalCase } from "change-case"; +import { createNamePolicy } from "../framework/core/name-policy.js"; +import { isDeclaration } from "../framework/utils/typeguards.js"; + +type NameKinds = "interfaceMember" | "interface" | "function" | "parameter"; + +export const typescriptNamePolicy = createNamePolicy((type, kind) => { + if (!("name" in type && typeof type.name === "string")) { + return "unknown name"; + } + + let name; + if (isTemplateInstance(type) && isDeclaration(type)) { + let unspeakable = false; + + const parameterNames = type.templateMapper.args.map((t) => { + if (t.entityKind === "Indeterminate") { + t = t.type; + } + if (!("kind" in t)) { + return undefined; + } + switch (t.kind) { + case "Model": + case "Scalar": + case "Interface": + case "Operation": + case "Enum": + case "Union": + case "Intrinsic": + if (!t.name) { + unspeakable = true; + return undefined; + } + const declName: string = typescriptNamePolicy.renamer.getName(t, "interface"); + if (declName === undefined) { + unspeakable = true; + return undefined; + } + return declName[0].toUpperCase() + declName.slice(1); + default: + unspeakable = true; + return undefined; + } + }); + + if (unspeakable) { + return "Unspeakable"; + } + + name = type.name + parameterNames.join(""); + } else { + name = type.name; + } + + switch (kind) { + case "interface": + return pascalCase(name); + case "interfaceMember": + case "function": + case "parameter": + return camelCase(name); + default: + throw new Error(`Unknown kind ${kind}`); + } +}); diff --git a/packages/efnext/src/typescript/record-expression.tsx b/packages/efnext/src/typescript/record-expression.tsx new file mode 100644 index 00000000000..1a17e2caa5c --- /dev/null +++ b/packages/efnext/src/typescript/record-expression.tsx @@ -0,0 +1,16 @@ +import { Type } from "@typespec/compiler"; +import { TypeExpression } from "./type-expression.js"; + +export interface RecordExpressionProps { + elementType: Type; +} + +export function RecordExpression({ elementType }: RecordExpressionProps) { + return ( + <> + Record{` + {`>`} + + ); +} diff --git a/packages/efnext/src/typescript/reference.tsx b/packages/efnext/src/typescript/reference.tsx new file mode 100644 index 00000000000..88b214e3a5e --- /dev/null +++ b/packages/efnext/src/typescript/reference.tsx @@ -0,0 +1,35 @@ +import { ScopeContext } from "../framework/components/scope.js"; +import { SourceFileContext } from "../framework/components/source-file.js"; +import { useContext } from "../framework/core/context.js"; + +export interface ReferenceProps { + refkey?: unknown; + builtin?: readonly [string, string] | [string, string]; +} +export async function Reference({ refkey, builtin }: ReferenceProps) { + const sourceFile = useContext(SourceFileContext); + + if (builtin) { + sourceFile!.addImport({ importPath: builtin[0], name: builtin[1], pathKind: "absolute" }); + return builtin[1]; + } + + const scope = useContext(ScopeContext); + if (!scope) { + throw new Error("Need scope context to form references"); + } + const binder = scope.binder; + + const result = await binder.resolveOrWaitForDeclaration(scope, refkey); + if (!result.resolved) { + throw new Error("Failed to resolve"); + } + + const { targetDeclaration, pathDown } = result; + if (pathDown.length > 0 && pathDown[0].kind === "module") { + // TODO update import + sourceFile!.addImport({ importPath: pathDown[0].path, name: targetDeclaration.name }); + } + + return targetDeclaration.name; +} diff --git a/packages/efnext/src/typescript/scalar-declaration.tsx b/packages/efnext/src/typescript/scalar-declaration.tsx new file mode 100644 index 00000000000..ac23e3aa39e --- /dev/null +++ b/packages/efnext/src/typescript/scalar-declaration.tsx @@ -0,0 +1,15 @@ +import { Scalar } from "@typespec/compiler"; +import { Declaration } from "../framework/components/declaration.js"; +import { TypeExpression } from "./type-expression.js"; + +export interface ScalarDeclarationProps { + type: Scalar; +} + +export function ScalarDeclaration({ type }: ScalarDeclarationProps) { + return ( + + export type {type.name} = ; + + ); +} diff --git a/packages/efnext/src/typescript/type-declaration.tsx b/packages/efnext/src/typescript/type-declaration.tsx new file mode 100644 index 00000000000..39839183bb8 --- /dev/null +++ b/packages/efnext/src/typescript/type-declaration.tsx @@ -0,0 +1,18 @@ +import { Type } from "@typespec/compiler"; +import { InterfaceDeclaration } from "./interface-declaration.js"; +import { UnionDeclaration } from "./union-declaration.js"; + +export interface TypeDeclarationProps { + type: Type; +} + +export function TypeDeclaration({ type }: TypeDeclarationProps) { + switch (type.kind) { + case "Model": + return ; + case "Union": + return ; + default: + throw new Error("Not yet supported"); + } +} diff --git a/packages/efnext/src/typescript/type-expression.tsx b/packages/efnext/src/typescript/type-expression.tsx new file mode 100644 index 00000000000..e1161b03359 --- /dev/null +++ b/packages/efnext/src/typescript/type-expression.tsx @@ -0,0 +1,101 @@ +import { IntrinsicType, Model, Scalar, Type } from "@typespec/compiler"; +import { isArray, isDeclaration, isRecord } from "../framework/utils/typeguards.js"; +import { ArrayExpression } from "./array-expression.js"; +import { InterfaceExpression } from "./interface-expression.js"; +import { RecordExpression } from "./record-expression.js"; +import { Reference } from "./reference.js"; +import { TypeLiteral } from "./type-literal.js"; +import { UnionExpression } from "./union-expression.js"; + +export interface TypeExpressionProps { + type: Type; +} + +export function TypeExpression({ type }: TypeExpressionProps) { + if (isDeclaration(type) && !(type as Model).indexer) { + // todo: probably need abstraction around deciding what's a declaration in the output + // (it may not correspond to things which are declarations in TypeSpec?) + return ; + } + + switch (type.kind) { + case "Scalar": + case "Intrinsic": + return <>{getScalarIntrinsicExpression(type)}; + case "Boolean": + case "Number": + case "String": + return ; + case "Union": + return ; + case "Tuple": + return ( + <> + [ + {type.values.map((element) => ( + <> + , + + ))} + ] + + ); + case "EnumMember": + return ( + <> + {type.enum.name}.{type.name} + + ); + case "Model": + if (isArray(type)) { + const elementType = type.indexer.value; + return ; + } + + if (isRecord(type)) { + const elementType = type.indexer.value; + return ; + } + + return ; + + default: + throw new Error(type.kind + " not supported in TypeExpression"); + } +} + +const intrinsicNameToTSType = new Map([ + ["unknown", "unknown"], + ["string", "string"], + ["int32", "number"], + ["int16", "number"], + ["float16", "number"], + ["integer", "number"], + ["float", "number"], + ["float32", "number"], + ["int64", "bigint"], + ["boolean", "boolean"], + ["null", "null"], + ["void", "void"], + ["numeric", "number"], + ["uint64", "number"], // TODO: bigint? + ["uint32", "number"], + ["uint16", "number"], + ["bytes", "Uint8Array"], + ["float64", "number"], // TODO: bigint? + ["safeint", "number"], + ["utcDateTime", "Date"], + ["url", "string"], +]); + +function getScalarIntrinsicExpression(type: Scalar | IntrinsicType): string { + if (type.kind === "Scalar" && type.baseScalar && type.namespace?.name !== "TypeSpec") { + // This is a delcared scalar + return ; + } + const tsType = intrinsicNameToTSType.get(type.name); + if (!tsType) { + throw new Error(`Unknown scalar type ${type.name}`); + } + return tsType; +} diff --git a/packages/efnext/src/typescript/type-literal.tsx b/packages/efnext/src/typescript/type-literal.tsx new file mode 100644 index 00000000000..f0bba4e4bca --- /dev/null +++ b/packages/efnext/src/typescript/type-literal.tsx @@ -0,0 +1,15 @@ +import { BooleanLiteral, NumericLiteral, StringLiteral } from "@typespec/compiler"; + +export interface TypeLiteralProps { + type: BooleanLiteral | StringLiteral | NumericLiteral; +} + +export function TypeLiteral({ type }: TypeLiteralProps) { + switch (type.kind) { + case "Boolean": + case "Number": + return String(type.value); + case "String": + return `"${type.value}"`; + } +} diff --git a/packages/efnext/src/typescript/union-declaration.tsx b/packages/efnext/src/typescript/union-declaration.tsx new file mode 100644 index 00000000000..01e654f297f --- /dev/null +++ b/packages/efnext/src/typescript/union-declaration.tsx @@ -0,0 +1,17 @@ +import { Enum, Union } from "@typespec/compiler"; +import { Declaration } from "../framework/components/declaration.js"; +import { UnionExpression } from "./union-expression.js"; + +export interface UnionDeclarationProps { + type: Union | Enum; + name?: string; +} + +export function UnionDeclaration({ type, name }: UnionDeclarationProps) { + const unionName = name ?? type.name!; + return ( + + export type {type.name} = ; + + ); +} diff --git a/packages/efnext/src/typescript/union-expression.tsx b/packages/efnext/src/typescript/union-expression.tsx new file mode 100644 index 00000000000..e75c01eef98 --- /dev/null +++ b/packages/efnext/src/typescript/union-expression.tsx @@ -0,0 +1,42 @@ +import { Enum, Union } from "@typespec/compiler"; +import { mapWithSep } from "../python/utils.js"; +import { TypeExpression } from "./type-expression.js"; +import { Value } from "./value.js"; + +export interface UnionExpressionProps { + type: Union | Enum; +} + +export function UnionExpression({ type }: UnionExpressionProps) { + if (type.kind === "Enum") { + const members = Array.from(type.members.values()); + return ( + <> + {mapWithSep( + members, + (member) => { + return ; + }, + " | " + )} + + ); + } else if (type.kind === "Union") { + const variants = Array.from(type.variants.values()); + return ( + <> + {variants.map((variant, index) => { + const isLast = index === variants.length - 1; + return ( + <> + {!isLast && " | "} + + ); + })} + + ); + } else { + // Should be unreachable + throw new Error(`Unexpected type kind: ${(type as any).kind}`); + } +} diff --git a/packages/efnext/src/typescript/value.tsx b/packages/efnext/src/typescript/value.tsx new file mode 100644 index 00000000000..4f06f671138 --- /dev/null +++ b/packages/efnext/src/typescript/value.tsx @@ -0,0 +1,108 @@ +import { code } from "#typespec/emitter/core"; + +export interface ValueProps { + jsValue?: unknown; + tspValue?: unknown; +} + +export function Value(props: ValueProps) { + const { tspValue, jsValue } = props; + + if ("jsValue" in props) { + if (isVerbatim(jsValue)) { + return jsValue.value; + } + switch (typeof jsValue) { + case "object": + if (Array.isArray(jsValue)) { + return + } else if (jsValue === null) { + return "null"; + } else { + return ; + } + case "boolean": + case "number": + return String(jsValue); + case "string": + return `"${jsValue}"`; + case "undefined": + return "undefined"; + } + } + + return `"Unknown Value"`; +} + +export interface ObjectValueProps { + jsValue?: object; +} + +export function ObjectValue({ jsValue }: ObjectValueProps) { + if (jsValue) { + const entries = Object.entries(jsValue); + + // Had to do this to be able to render the empty object + if (entries.length === 0) { + return <>{"{ }"}; + } + + const val = Object.entries(jsValue) + .map(([key, jsPropValue]) => { + return ; + }) + .reduce((prev, curr) => { + // This prevents a trailing comma + if (!prev || prev.length === 0) { + return [curr]; + } + return [prev, ", ", curr]; + }, []); + + return ["{", val, "}"]; + } + return <>; +} + +export interface ArrayValueProps { + jsValue: unknown[]; +} +export function ArrayValue({jsValue}: ArrayValueProps) { + const itemValues = jsValue.map(v => <>,); + + return code` + [${itemValues}] + ` +} + +export interface ObjectValuePropertyProps { + name?: string; + jsPropertyValue?: unknown; +} + +ObjectValue.Property = function Property({ name, jsPropertyValue }: ObjectValuePropertyProps) { + return ( + <> + {name}: + + ); +}; + +export function NullValue() { + return "null"; +} + + +const verbatimSym = Symbol(); +export function isVerbatim(v: unknown): v is Verbatim { + return typeof v === "object" && v !== null && (v as any)[verbatimSym]; +} + +export interface Verbatim { + [verbatimSym]: true, + value: string +} + +export function $verbatim(s: string): Verbatim { + return { [verbatimSym]: true, value: s } +} \ No newline at end of file diff --git a/packages/efnext/test/component-utils.tsx b/packages/efnext/test/component-utils.tsx new file mode 100644 index 00000000000..30085cbb7bb --- /dev/null +++ b/packages/efnext/test/component-utils.tsx @@ -0,0 +1,33 @@ +import { format } from "prettier"; +import { assert } from "vitest"; +import { EmitOutput } from "../src/framework/components/emit-output.js"; +import { SourceFile } from "../src/framework/components/source-file.js"; +import { render, RenderedTreeNode } from "../src/framework/core/render.js"; + +async function prepareExpected(expected: string) { + const expectedRoot = ( + + + {expected} + + + ); + + const rendered = await render(expectedRoot); + const raw = (rendered as any).flat(Infinity).join(""); + + return format(raw, { parser: "typescript" }); +} + +async function prepareActual(actual: RenderedTreeNode) { + const raw = (actual as any).flat(Infinity).join(""); + + return format(raw, { parser: "typescript" }); +} + +export async function assertEqual(actual: RenderedTreeNode, expected: string) { + const actualFormatted = await prepareActual(actual); + const expectedFormatted = await prepareExpected(expected); + + assert.equal(actualFormatted, expectedFormatted); +} diff --git a/packages/efnext/test/context.test.tsx b/packages/efnext/test/context.test.tsx new file mode 100644 index 00000000000..d2bbb309de2 --- /dev/null +++ b/packages/efnext/test/context.test.tsx @@ -0,0 +1,25 @@ +import assert, { strictEqual } from "node:assert"; +import { describe, it } from "vitest"; +import { setTimeout } from "node:timers/promises" +import { render } from "../src/framework/core/render.js"; +import { createContext, useContext } from "../src/framework/core/context.js"; + +describe("context api", () => { + it("can get context from a parent node", async () => { + const TestContext = createContext(); + + function Test() { + return + + + } + + function Test2() { + const value = useContext(TestContext); + return value; + } + + const tree = await render(); + console.log(tree); + }) +}) diff --git a/packages/efnext/test/e2e.test.tsx b/packages/efnext/test/e2e.test.tsx new file mode 100644 index 00000000000..e9e0b3a2295 --- /dev/null +++ b/packages/efnext/test/e2e.test.tsx @@ -0,0 +1,132 @@ +import { describe, it } from "vitest"; +import { EmitOutput } from "../src/framework/components/emit-output.js"; +import { SourceFile } from "../src/framework/components/source-file.js"; +import { + SourceFileRecord as SF, + render, + renderToSourceFiles, +} from "../src/framework/core/render.js"; +import { Reference } from "../src/typescript/reference.js"; +import { TypeDeclaration } from "../src/typescript/type-declaration.js"; +import { getProgram } from "./test-host.js"; +import { print } from "./utils.js"; + +function printSourceFiles(files: SF[]) { + for (const file of files) { + console.log("## " + file.path); + console.log((file.content += "\n")); + } +} + +describe("e2e", () => { + it("example", async () => { + const program = await getProgram(` + model Foo { x: true | string } + model Bar { x: Foo, y: { a: 1 } } + `); + + const [Foo] = program.resolveTypeReference("Foo"); + const [Bar] = program.resolveTypeReference("Bar"); + + let res = await renderToSourceFiles( + + + + + + + + + const x = ; + + + ); + + printSourceFiles(res); + }); + + it("interfaces", async () => { + const program = await getProgram(` + model Foo { x: true } + model Bar { x: Foo, y: { a: 1 } } + `); + + const [Foo] = program.resolveTypeReference("Foo"); + const [Bar] = program.resolveTypeReference("Bar"); + + let res = await render( + + + + + + + ); + + await print(res); + }); + it("interfaces in reverse order", async () => { + const program = await getProgram(` + model Foo { x: true } + model Bar { x: Foo, y: { a: 1 } } + `); + + const [Foo] = program.resolveTypeReference("Foo"); + const [Bar] = program.resolveTypeReference("Bar"); + + let res = await render( + + + + + + + ); + + await print(res); + }); + + it("works with circular references", async () => { + const program = await getProgram(` + model Foo { x: Bar } + model Bar { x: Foo } + `); + + const [Foo] = program.resolveTypeReference("Foo"); + const [Bar] = program.resolveTypeReference("Bar"); + + let res = await render( + + + + + + + ); + + await print(res); + }); + + it("works with cross-source-file references", async () => { + const program = await getProgram(` + model Foo { x: Bar } + model Bar { x: Foo } + `); + + const [Foo] = program.resolveTypeReference("Foo"); + const [Bar] = program.resolveTypeReference("Bar"); + + let res = await render( + + + + + + + + + ); + + await print(res); + }); +}); diff --git a/packages/efnext/test/function-declaration.test.tsx b/packages/efnext/test/function-declaration.test.tsx new file mode 100644 index 00000000000..566873173aa --- /dev/null +++ b/packages/efnext/test/function-declaration.test.tsx @@ -0,0 +1,137 @@ +import { Namespace } from "@typespec/compiler"; +import { describe, it } from "vitest"; +import { EmitOutput } from "../src/framework/components/emit-output.js"; +import { SourceFile } from "../src/framework/components/source-file.js"; +import { code } from "../src/framework/core/code.js"; +import { render } from "../src/framework/core/render.js"; +import { FunctionDeclaration } from "../src/typescript/function-declaration.js"; +import { assertEqual } from "./component-utils.js"; +import { getProgram } from "./test-host.js"; + +describe("Typescript Function Declaration", () => { + describe("Function bound to Typespec Types", () => { + describe("Bound to Operation", () => { + it("creates a function", async () => { + const program = await getProgram(` + namespace DemoService; + op getName(id: string): string; + `); + + const [namespace] = program.resolveTypeReference("DemoService"); + const operation = Array.from((namespace as Namespace).operations.values())[0]; + + let res = await render( + + + + + + ); + + await assertEqual(res, `export function getName(id: string): string{}`); + }); + + it("can override name", async () => { + const program = await getProgram(` + namespace DemoService; + op getName(id: string): string; + `); + + const [namespace] = program.resolveTypeReference("DemoService"); + const operation = Array.from((namespace as Namespace).operations.values())[0]; + + let res = await render( + + + + + + ); + + await assertEqual(res, `export function newName(id: string): string{}`); + }); + + it("can override parameters with raw params provided", async () => { + const program = await getProgram(` + namespace DemoService; + op createPerson(id: string): string; + `); + + const [namespace] = program.resolveTypeReference("DemoService"); + const operation = Array.from((namespace as Namespace).operations.values())[0]; + + let res = await render( + + + + + + ); + + await assertEqual(res, `export function createPerson(name: string, age: number): string{}`); + }); + + it("can override parameters with an array of ModelProperties", async () => { + const program = await getProgram(` + namespace DemoService; + op createPerson(id: string): string; + + model Foo { + name: string; + age: int32; + } + `); + + const [namespace] = program.resolveTypeReference("DemoService"); + const operation = Array.from((namespace as Namespace).operations.values())[0]; + const model = Array.from((namespace as Namespace).models.values())[0]; + + let res = await render( + + + + + + + + ); + + await assertEqual(res, `export function createPerson(name: string, age: number): string{}`); + }); + + it("can render function body", async () => { + const program = await getProgram(` + namespace DemoService; + op createPerson(id: string): string; + `); + + const [namespace] = program.resolveTypeReference("DemoService"); + const operation = Array.from((namespace as Namespace).operations.values())[0]; + + let res = await render( + + + + {code` + const message = "Hello World!"; + console.log(message); + `} + + + + ); + + await assertEqual( + res, + `export function createPerson(id: string): string { + const message = "Hello World!"; + console.log(message); + }` + ); + }); + }); + }); +}); diff --git a/packages/efnext/test/hello.test.tsx b/packages/efnext/test/hello.test.tsx new file mode 100644 index 00000000000..e77a7cbd561 --- /dev/null +++ b/packages/efnext/test/hello.test.tsx @@ -0,0 +1,9 @@ +import { describe, it } from "vitest"; +import { emit } from "./test-host.js"; + +describe.skip("hello", () => { + it("emit output.txt with content hello world", async () => { + const results = await emit(`op test(colors: boolean): void;`); + console.log(results); + }); +}); diff --git a/packages/efnext/test/indent.test.tsx b/packages/efnext/test/indent.test.tsx new file mode 100644 index 00000000000..453863da273 --- /dev/null +++ b/packages/efnext/test/indent.test.tsx @@ -0,0 +1,30 @@ +import { describe, it } from "vitest"; +import { render, print } from "../src/framework/core/render.js"; +import { Indent } from "../src/framework/components/indent.js"; +import assert from "node:assert/strict"; + +describe("", () => { + it("indents appropriately", async () => { + const tree = await render( + + @dataclass
+ class Address:
+ + street: str
+ city: str
+ postal_code: str
+
+
+ ) + + let str = await print(tree); + assert.equal(str, +` @dataclass + class Address: + street: str + city: str + postal_code: str +` + ) + }) +}) \ No newline at end of file diff --git a/packages/efnext/test/interface.test.tsx b/packages/efnext/test/interface.test.tsx new file mode 100644 index 00000000000..85202b654a0 --- /dev/null +++ b/packages/efnext/test/interface.test.tsx @@ -0,0 +1,351 @@ +import { Namespace } from "@typespec/compiler"; +import { describe, it } from "vitest"; +import { EmitOutput } from "../src/framework/components/emit-output.js"; +import { SourceFile } from "../src/framework/components/source-file.js"; +import { render } from "../src/framework/core/render.js"; +import { InterfaceDeclaration } from "../src/typescript/interface-declaration.js"; +import { assertEqual } from "./component-utils.js"; +import { getProgram } from "./test-host.js"; + +describe("Typescript Interface", () => { + describe("Interface bound to Typespec Types", () => { + describe("Bound to Model", () => { + it("creates an interface", async () => { + const program = await getProgram(` + namespace DemoService; + + model Widget{ + id: string; + weight: int32; + color: "blue" | "red"; + } + `); + + const [namespace] = program.resolveTypeReference("DemoService"); + const models = Array.from((namespace as Namespace).models.values()); + + let res = await render( + + + + + + ); + + await assertEqual( + res, + `export interface Widget { + id: string; + weight: number; + color: "blue" | "red"; + }` + ); + }); + + it("can override interface name", async () => { + const program = await getProgram(` + namespace DemoService; + + model Widget{ + id: string; + weight: int32; + color: "blue" | "red"; + } + `); + + const [namespace] = program.resolveTypeReference("DemoService"); + const models = Array.from((namespace as Namespace).models.values()); + + let res = await render( + + + + + + ); + + await assertEqual( + res, + `export interface MyOperations { + id: string; + weight: number; + color: "blue" | "red"; + }` + ); + }); + + it("can add a members to the interface", async () => { + const program = await getProgram(` + namespace DemoService; + + model Widget{ + id: string; + weight: int32; + color: "blue" | "red"; + } + `); + + const [namespace] = program.resolveTypeReference("DemoService"); + const models = Array.from((namespace as Namespace).models.values()); + + let res = await render( + + + + customProperty: string;
+ customMethod(): void; +
+
+
+ ); + + await assertEqual( + res, + `export interface MyOperations { + id: string; + weight: number; + color: "blue" | "red"; + customProperty: string; + customMethod(): void; + }` + ); + }); + + it("interface name can be customized", async () => { + const program = await getProgram(` + namespace DemoService; + + model Widget{ + id: string; + weight: int32; + color: "blue" | "red"; + } + `); + + const [namespace] = program.resolveTypeReference("DemoService"); + const models = Array.from((namespace as Namespace).models.values()); + + let res = await render( + + + + + + ); + + await assertEqual( + res, + `export interface MyModel { + id: string; + weight: number; + color: "blue" | "red"; + }` + ); + }); + + it("interface with extends", async () => { + const program = await getProgram(` + namespace DemoService; + + model Widget{ + id: string; + weight: int32; + color: "blue" | "red"; + } + + model ErrorWidget extends Widget { + code: int32; + message: string; + } + `); + + const [namespace] = program.resolveTypeReference("DemoService"); + const models = Array.from((namespace as Namespace).models.values()); + + let res = await render( + + + {models.map((model) => ( + + ))} + + + ); + + await assertEqual( + res, + `export interface Widget { + id: string; + weight: number; + color: "blue" | "red"; + } + export interface ErrorWidget extends Widget { + code: number; + message: string; + }` + ); + }); + }); + + describe("Bound to Interface", () => { + it("creates an interface", async () => { + const program = await getProgram(` + namespace DemoService; + + interface WidgetOperations { + op getName(id: string): string; + } + `); + + const [namespace] = program.resolveTypeReference("DemoService"); + const interfaces = Array.from((namespace as Namespace).interfaces.values()); + + let res = await render( + + + + + + ); + + await assertEqual( + res, + `export interface WidgetOperations { + getName(id: string): string; + }` + ); + }); + + it("should handle spread and non spread model parameters", async () => { + const program = await getProgram(` + namespace DemoService; + + model Foo { + name: string + } + + interface WidgetOperations { + op getName(foo: Foo): string; + op getOtherName(...Foo): string + } + `); + + const [namespace] = program.resolveTypeReference("DemoService"); + const interfaces = Array.from((namespace as Namespace).interfaces.values()); + const models = Array.from((namespace as Namespace).models.values()); + + let res = await render( + + + + + + + ); + + await assertEqual( + res, + `export interface WidgetOperations { + getName(foo: Foo): string; + getOtherName(name: string): string + } + export interface Foo { + name: string; + } + ` + ); + }); + + it("creates an interface with Model references", async () => { + const program = await getProgram(` + namespace DemoService; + + interface WidgetOperations { + op getName(id: string): Widget; + } + + model Widget { + id: string; + weight: int32; + color: "blue" | "red"; + } + `); + + const [namespace] = program.resolveTypeReference("DemoService"); + const interfaces = Array.from((namespace as Namespace).interfaces.values()); + const models = Array.from((namespace as Namespace).models.values()); + + let res = await render( + + + + {models.map((model) => ( + + ))} + + + ); + + await assertEqual( + res, + `export interface WidgetOperations { + getName(id: string): Widget; + } + export interface Widget { + id: string; + weight: number; + color: "blue" | "red"; + }` + ); + }); + + it("creates an interface that extends another", async () => { + const program = await getProgram(` + namespace DemoService; + + interface WidgetOperations { + op getName(id: string): Widget; + } + + interface WidgetOperationsExtended extends WidgetOperations{ + op delete(id: string): void; + } + + model Widget { + id: string; + weight: int32; + color: "blue" | "red"; + } + `); + + const [namespace] = program.resolveTypeReference("DemoService"); + const interfaces = Array.from((namespace as Namespace).interfaces.values()); + const models = Array.from((namespace as Namespace).models.values()); + + let res = await render( + + + + {models.map((model) => ( + + ))} + + + ); + + await assertEqual( + res, + `export interface WidgetOperationsExtended { + getName(id: string): Widget; + delete(id: string): void; + } + export interface Widget { + id: string; + weight: number; + color: "blue" | "red"; + }` + ); + }); + }); + }); +}); diff --git a/packages/efnext/test/mutators.test.tsx b/packages/efnext/test/mutators.test.tsx new file mode 100644 index 00000000000..663e26a653e --- /dev/null +++ b/packages/efnext/test/mutators.test.tsx @@ -0,0 +1,120 @@ +import { ModelProperty, Mutator, MutatorFlow, Operation, mutateSubgraph } from "@typespec/compiler"; +import { getHttpOperation } from "@typespec/http"; +import { assert, describe, it } from "vitest"; +import { render } from "../src/framework/core/render.js"; +import { emitTypescriptInterfaces } from "../src/typescript-interface-emitter.js"; +import { assertEqual } from "./component-utils.js"; +import { getProgram } from "./test-host.js"; + +// This is a mutator that changes the operation shape into the shape of an RLC path operation. +export function createRlcMutator(kind: "operation"): Mutator { + return { + name: kind + "Rlc", + + Operation: { + filter(o, program, realm) { + const [httpOperation] = getHttpOperation(program, o); + if (!httpOperation) { + return MutatorFlow.DontMutate; + } + + return true; + }, + mutate(o, clone, program, realm) { + const [httpOperation] = getHttpOperation(program, o); + + if (clone.name) { + clone.name = "path"; + } + + const decoratorArgs: any[] = [ + /* decorator arguments */ + ]; + + const modelOptions = { + extends: undefined, // or some base model + namespace: undefined, // or some namespace + // other options + }; + + const modelPropeties: ModelProperty[] = []; + + const pathType = realm.typeFactory.stringLiteral(httpOperation.path); + const params = realm.typeFactory.modelProperty(...decoratorArgs, "pathParam", pathType); + + modelPropeties.push(params); + + const paramsModel = realm.typeFactory.model( + ...decoratorArgs, + "", + modelPropeties, + modelOptions + ); + realm.typeFactory.finishType(paramsModel); + clone.parameters = paramsModel; + }, + }, + }; +} + +describe("e2e operation mutator", () => { + it("should not mutate operation name if no http", async () => { + const program = await getProgram( + ` + interface A { + foo(): void; + } + `, + { libraries: ["Http"] } + ); + + const result = await render(emitTypescriptInterfaces(program)); + + await assertEqual( + result, + `export interface A { + foo(): void + }` + ); + }); + + it("should mutate operation if http", async () => { + const program = await getProgram( + ` + import "@typespec/http"; + + using TypeSpec.Http; + @service({ + title: "Widget Service", + }) + namespace DemoService; + + @route("/widgets") + @tag("Widgets") + interface A { + @get foo(): void; + } + `, + { libraries: ["Http"] } + ); + + const namespace = Array.from(program.getGlobalNamespaceType().namespaces.values()).filter( + (n) => n.name === "DemoService" + )[0]; + const iface = Array.from(namespace.interfaces.values()).find((i) => i.name === "A")!; + const operation = Array.from(iface.operations.values())[0]; + + const { type } = mutateSubgraph(program, [createRlcMutator("operation")], operation); + const mutatedOperation = type as Operation; + + assert.equal(mutatedOperation.name, "path"); + + const parameters = [...mutatedOperation.parameters.properties.values()].map((m) => [ + m.name, + (m.type as any).value, + ]); + const [paramName, path] = parameters[0]; + assert.equal(path, "/widgets"); + assert.equal(paramName, "pathParam"); + }); +}); diff --git a/packages/efnext/test/on-resolved.test.tsx b/packages/efnext/test/on-resolved.test.tsx new file mode 100644 index 00000000000..c40fa32088f --- /dev/null +++ b/packages/efnext/test/on-resolved.test.tsx @@ -0,0 +1,18 @@ +import { describe, it } from "vitest"; +import { render } from "../src/framework/core/render.js"; +import { useResolved } from "../src/framework/core/use-resolved.js"; +import assert from "node:assert/strict" + +describe("useResolved", () => { + it("works", async () => { + function Test() { + const OnResolved = useResolved(() => <> + hi + ); + return + } + + const res = await render(); + assert.deepEqual(res, [[["hi"]]]); + }); +}); \ No newline at end of file diff --git a/packages/efnext/test/render.test.tsx b/packages/efnext/test/render.test.tsx new file mode 100644 index 00000000000..fb43b736b53 --- /dev/null +++ b/packages/efnext/test/render.test.tsx @@ -0,0 +1,177 @@ +import assert from "node:assert"; +import { setTimeout } from "node:timers/promises"; +import { describe, it } from "vitest"; +import { render } from "../src/framework/core/render.js"; +import test from "node:test"; + +describe("render", () => { + describe("component return types", () => { + describe("primitive values", () => { + it("can return nothing, which is ignored", async () => { + function Test() {} + const test = 1; + const rt = await render(); + assert.deepStrictEqual(rt, []); + }); + + it("can return a string", async () => { + function Test() { + return "hi"; + } + const rt = await render(); + assert.deepStrictEqual(rt, ["hi"]); + }); + + it("can return a number, which is stringified", async () => { + function Test() { + return 1; + } + const rt = await render(); + assert.deepStrictEqual(rt, ["1"]); + }); + + // NB: I think this should be ignored. + it("can return false, which is ignored", async () => { + function Test() { + return false; + } + const rt = await render(); + assert.deepStrictEqual(rt, []); + }); + + it("can return true, which is ignored", async () => { + function Test() { + return true; + } + const rt = await render(); + assert.deepStrictEqual(rt, []); + }); + }); + //describe("array values", () => {}); + describe("components", () => { + it("can return a single component", async () => { + function Outer() { + return ; + } + + function Inner() { + return "hi"; + } + + const rt = await render(); + assert.deepStrictEqual(rt, [["hi"]]); + }); + + it("can return a fragment, which doesn't add a node", async () => { + function Test() { + return ( + <> + <> + <> +
+ + + + ); + } + + console.log(await render()); + }); + + it("can return multiple component in a fragment", async () => { + function Test() { + return ( + <> +
+
+ + ); + } + + const rt = await render(); + assert.deepStrictEqual(rt, [[["\n"], ["\n"]]]); + }); + + it("await renders non-SourceNode children properly", async () => { + function Test() { + return ( + <> + hello + + + + + + ); + } + + function Foo({ children }: any) { + return children; + } + + function Bar() { + return "bar"; + } + + const rt = await render(); + assert.deepStrictEqual(rt, [["hello", [["bar"], ["bar"]]]]); + }); + + it("await renders SourceNode children properly", async () => { + function Test() { + return ( + <> + hello + + + + + + ); + } + + function Foo({ children }: any) { + return ( + <> + + {children} + + + ); + } + + function Bar() { + return
; + } + + const rt = await render(); + console.log(JSON.stringify(rt, null, 4)); + }); + }); + + describe("async components", () => { + it("can handle promises for strings", async () => { + const p = setTimeout(10, "hi!"); + function Foo() { + return <>{p} there!; + } + + const rt = await render(); + await p; + assert.deepStrictEqual(rt, [["hi!", " there!"]]); + }); + + it("works with async function components", async () => { + const p = setTimeout(10, "hi!"); + async function Foo() { + await p; + return <>{p} there!; + } + + const rt = await render(); + await setTimeout(11); // terrible, need a way to await the tree being settled. + assert.deepStrictEqual(rt, [["hi!", " there!"]]); + }); + }); + }); +}); diff --git a/packages/efnext/test/scope.test.tsx b/packages/efnext/test/scope.test.tsx new file mode 100644 index 00000000000..3c194330f61 --- /dev/null +++ b/packages/efnext/test/scope.test.tsx @@ -0,0 +1,42 @@ +import { strict as assert } from "node:assert"; +import { describe, it } from "vitest"; +import { render } from "../src/framework/core/render.js"; +import { Scope, ScopeContext } from "../src/framework/components/scope.js"; +import { useContext } from "../src/framework/core/context.js"; +import { EmitOutput } from "../src/framework/components/emit-output.js"; + +describe("Scope component", () => { + it("provides scope", () => { + function Test() { + const currentScope = useContext(ScopeContext); + assert(currentScope); + assert.equal(currentScope.name, "test"); + assert.equal(currentScope.parent!.kind, "global"); + } + + render( + + + + ); + }); + + it("sets nested context", () => { + function Test() { + const currentScope = useContext(ScopeContext); + assert(currentScope); + assert(currentScope.kind === "local"); + assert(currentScope.parent.kind === "local"); + assert.equal(currentScope.name, "child"); + assert.equal(currentScope.parent.name, "parent"); + } + + render( + + + + + + ); + }) +}) \ No newline at end of file diff --git a/packages/efnext/test/source-directory.test.tsx b/packages/efnext/test/source-directory.test.tsx new file mode 100644 index 00000000000..b6995752559 --- /dev/null +++ b/packages/efnext/test/source-directory.test.tsx @@ -0,0 +1,61 @@ +import { assert, describe, it } from "vitest"; +import { Declaration } from "../src/framework/components/declaration.js"; +import { EmitOutput } from "../src/framework/components/emit-output.js"; +import { SourceDirectory } from "../src/framework/components/source-directory.js"; +import { SourceFile } from "../src/framework/components/source-file.js"; +import { renderToSourceFiles } from "../src/framework/core/render.js"; +import { Reference } from "../src/typescript/reference.js"; + +describe("Source directory component", () => { + it("works", async () => { + let res = await renderToSourceFiles( + + + + + const hi = 1; + + + + const bye = ; + + + + ); + + assert.lengthOf(res, 2); + assert.include( + res.map((x) => x.path), + "src/test1.ts" + ); + assert.include( + res.map((x) => x.path), + "src/test2.ts" + ); + }); + + it("works with no directory", async () => { + let res = await renderToSourceFiles( + + + + const hi = 1; + + + + const bye = ; + + + ); + + assert.lengthOf(res, 2); + assert.include( + res.map((x) => x.path), + "test1.ts" + ); + assert.include( + res.map((x) => x.path), + "test2.ts" + ); + }); +}); diff --git a/packages/efnext/test/source-file.test.tsx b/packages/efnext/test/source-file.test.tsx new file mode 100644 index 00000000000..98529994c82 --- /dev/null +++ b/packages/efnext/test/source-file.test.tsx @@ -0,0 +1,27 @@ +import { strict as assert } from "node:assert"; +import { describe, it } from "vitest"; +import { render } from "../src/framework/core/render.js"; +import { Scope, ScopeContext } from "../src/framework/components/scope.js"; +import { useContext } from "../src/framework/core/context.js"; +import { EmitOutput } from "../src/framework/components/emit-output.js"; +import { SourceFile } from "../src/framework/components/source-file.js"; +import { Declaration } from "../src/framework/components/declaration.js"; +import { Reference } from "../src/typescript/reference.js"; +import { print } from "./utils.js"; + +describe("Source file component", () => { + it("works", async () => { + let res = await render( + + + const hi = 1; + + + + const bye = ; + + ); + + await print(res); + }) +}) \ No newline at end of file diff --git a/packages/efnext/test/string-templates.test.tsx b/packages/efnext/test/string-templates.test.tsx new file mode 100644 index 00000000000..822edf8e69a --- /dev/null +++ b/packages/efnext/test/string-templates.test.tsx @@ -0,0 +1,39 @@ +import { describe, it } from "vitest"; +import { render, print } from "../src/framework/core/render.js"; +import { Indent } from "../src/framework/components/indent.js"; +import assert from "node:assert/strict"; +import { code } from "../src/framework/core/code.js"; + +describe("code template", () => { + it("works", async () => { + function PythonDataClass() { + return code` + @dataclass + class Foo: + ${} + isFoo: bool + + @dataclass + class Bar: + ${} + isBar: bool + ` + } + + function StandardMembers() { + return code` + ${} + ${} + ` + } + function DataClassMember({ name, type }: { name: string, type: string }) { + return <>{name}: {type}; + } + + + const tree = await render(); + const output = await print(tree); + console.log(output); + //assert.equal(output, "A\nB\nC"); + }); +}); \ No newline at end of file diff --git a/packages/efnext/test/test-host.ts b/packages/efnext/test/test-host.ts new file mode 100644 index 00000000000..1c7641ab2d4 --- /dev/null +++ b/packages/efnext/test/test-host.ts @@ -0,0 +1,69 @@ +import { Diagnostic, Program, resolvePath } from "@typespec/compiler"; +import { + createTestHost, + createTestWrapper, + expectDiagnosticEmpty, +} from "@typespec/compiler/testing"; +import { HttpTestLibrary } from "@typespec/http/testing"; +import { TestLibrary } from "../src/testing/index.js"; + +export async function createTypespecCliTestHost( + options: { libraries: "Http"[] } = { libraries: [] } +) { + const libraries = [TestLibrary]; + if (options.libraries.includes("Http")) { + libraries.push(HttpTestLibrary); + } + return createTestHost({ + libraries, + }); +} + +export async function createTypespecCliTestRunner() { + const host = await createTypespecCliTestHost(); + + return createTestWrapper(host, { + compilerOptions: { + noEmit: false, + emit: ["@typespec/efnext"], + }, + }); +} + +export async function emitWithDiagnostics( + code: string +): Promise<[Record, readonly Diagnostic[]]> { + const runner = await createTypespecCliTestRunner(); + await runner.compileAndDiagnose(code, { + outputDir: "tsp-output", + }); + const emitterOutputDir = "./tsp-output/@typespec/efnext"; + const files = await runner.program.host.readDir(emitterOutputDir); + + const result: Record = {}; + for (const file of files) { + result[file] = (await runner.program.host.readFile(resolvePath(emitterOutputDir, file))).text; + } + return [result, runner.program.diagnostics]; +} + +export async function emit(code: string): Promise> { + const [result, diagnostics] = await emitWithDiagnostics(code); + expectDiagnosticEmpty(diagnostics); + return result; +} + +export async function getProgram( + code: string, + options: { libraries: "Http"[] } = { libraries: [] } +): Promise { + const host = await createTypespecCliTestHost(options); + const wrapper = createTestWrapper(host, { + compilerOptions: { + noEmit: true, + }, + }); + const [_, diagnostics] = await wrapper.compileAndDiagnose(code); + expectDiagnosticEmpty(diagnostics); + return wrapper.program; +} diff --git a/packages/efnext/test/typescript-components/intersection-declaration.test.tsx b/packages/efnext/test/typescript-components/intersection-declaration.test.tsx new file mode 100644 index 00000000000..52a82782161 --- /dev/null +++ b/packages/efnext/test/typescript-components/intersection-declaration.test.tsx @@ -0,0 +1,187 @@ +import { Namespace } from "@typespec/compiler"; +import { describe, it } from "vitest"; +import { EmitOutput } from "../../src/framework/components/emit-output.js"; +import { SourceFile } from "../../src/framework/components/source-file.js"; +import { render } from "../../src/framework/core/render.js"; +import { InterfaceDeclaration } from "../../src/typescript/interface-declaration.js"; +import { + IntersectionConstituent, + IntersectionDeclaration, +} from "../../src/typescript/intersection-declaration.js"; +import { assertEqual } from "../component-utils.js"; +import { getProgram } from "../test-host.js"; + +describe("Intersection Declaration", () => { + it("should render a type declaration for a model", async () => { + const program = await getProgram(` + namespace DemoService; + + model Widget{ + id: string; + weight: int32; + color: "blue" | "red"; + } + `); + + const [namespace] = program.resolveTypeReference("DemoService"); + const models = Array.from((namespace as Namespace).models.values()); + + let res = await render( + + + + + + ); + + await assertEqual( + res, + `export type Widget = { id: string; weight: number; color: "blue" | "red" };` + ); + }); + + it("should render a type declaration for a model and a constituent", async () => { + const program = await getProgram(` + namespace DemoService; + + model Widget{ + id: string; + weight: int32; + color: "blue" | "red"; + } + + model Telemetry { + eventId: string; + } + `); + + const [namespace] = program.resolveTypeReference("DemoService"); + const models = Array.from((namespace as Namespace).models.values()); + const widget = models[0]; + const telemetry = models[1]; + + let res = await render( + + + + + + + + + ); + + await assertEqual( + res, + `export interface Telemetry { + eventId: string; + } + export type Widget = Telemetry & { + id: string; + weight: number; + color: "blue" | "red"; + };` + ); + }); + + it("should render a type declaration for a model and multiple constituents", async () => { + const program = await getProgram(` + namespace DemoService; + + model Widget{ + id: string; + weight: int32; + color: "blue" | "red"; + } + + model Telemetry { + eventId: string; + } + + model Logging { + logLevel: string; + } + `); + + const [namespace] = program.resolveTypeReference("DemoService"); + const models = Array.from((namespace as Namespace).models.values()); + const widget = models[0]; + const telemetry = models[1]; + const logging = models[2]; + + let res = await render( + + + + + + + + + + + ); + + await assertEqual( + res, + `export interface Logging { + logLevel: string; + } + export interface Telemetry { + eventId: string; + } + export type Widget = Telemetry & Logging & { id: string; weight: number; color: "blue" | "red";};` + ); + }); + + it("should render a type declaration for a model with constituents, and honor children", async () => { + const program = await getProgram(` + namespace DemoService; + + model Widget{ + id: string; + weight: int32; + color: "blue" | "red"; + } + + model Telemetry { + eventId: string; + } + + model Logging { + logLevel: string; + } + `); + + const [namespace] = program.resolveTypeReference("DemoService"); + const models = Array.from((namespace as Namespace).models.values()); + const widget = models[0]; + const telemetry = models[1]; + const logging = models[2]; + + let res = await render( + + + + + + + + myProp: string; + + + + ); + + await assertEqual( + res, + `export interface Logging { + logLevel: string; + } + export interface Telemetry { + eventId: string; + } + export type Widget = Telemetry & Logging & { id: string; weight: number; color: "blue" | "red"; myProp: string};` + ); + }); +}); diff --git a/packages/efnext/test/utils.ts b/packages/efnext/test/utils.ts new file mode 100644 index 00000000000..bbc17e95f03 --- /dev/null +++ b/packages/efnext/test/utils.ts @@ -0,0 +1,13 @@ +import { format } from "prettier"; +import { RenderedTreeNode } from "../src/framework/core/render.js"; + +export async function print(root: RenderedTreeNode) { + const raw = (root as any).flat(Infinity).join(""); + + try { + console.log(await format(raw, { parser: "typescript" })); + } catch (e) { + console.error("Formatting error", e); + console.log(raw); + } +} diff --git a/packages/efnext/tsconfig.json b/packages/efnext/tsconfig.json new file mode 100644 index 00000000000..2dd35a17d57 --- /dev/null +++ b/packages/efnext/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "references": [{ "path": "../compiler/tsconfig.json" }], + "compilerOptions": { + "outDir": "dist", + "rootDir": ".", + "tsBuildInfoFile": "temp/tsconfig.tsbuildinfo", + "jsx": "react-jsx", + "jsxImportSource": "#jsx", + "sourceMap": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "generated-defs/**/*.ts", "test/**/*.ts", "test/**/*.tsx", "src/index.tsx", "test/hello.test.tsx"] +} diff --git a/packages/eslint-plugin-typespec/CHANGELOG.md b/packages/eslint-plugin-typespec/CHANGELOG.md index 0724012f372..4a10be0b269 100644 --- a/packages/eslint-plugin-typespec/CHANGELOG.md +++ b/packages/eslint-plugin-typespec/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log - @typespec/eslint-plugin +## 0.57.0 + +### Bump dependencies + +- [#3401](https://github.com/microsoft/typespec/pull/3401) Update dependencies - May 2024 + + ## 0.56.0 ### Bump dependencies diff --git a/packages/eslint-plugin-typespec/package.json b/packages/eslint-plugin-typespec/package.json index a8a50edc32d..9c7b55a964c 100644 --- a/packages/eslint-plugin-typespec/package.json +++ b/packages/eslint-plugin-typespec/package.json @@ -1,6 +1,6 @@ { "name": "@typespec/eslint-plugin", - "version": "0.56.0", + "version": "0.57.0", "author": "Microsoft Corporation", "description": "Eslint plugin providing set of rules to be used in the JS/TS code of TypeSpec libraries", "homepage": "https://typespec.io", diff --git a/packages/html-program-viewer/CHANGELOG.md b/packages/html-program-viewer/CHANGELOG.md index 2c4209915be..a5aca219d97 100644 --- a/packages/html-program-viewer/CHANGELOG.md +++ b/packages/html-program-viewer/CHANGELOG.md @@ -1,5 +1,16 @@ # Change Log - @typespec/html-program-viewer +## 0.57.0 + +### Bump dependencies + +- [#3401](https://github.com/microsoft/typespec/pull/3401) Update dependencies - May 2024 + +### Features + +- [#3022](https://github.com/microsoft/typespec/pull/3022) Add support for values + + ## 0.56.0 ### Bump dependencies diff --git a/packages/html-program-viewer/package.json b/packages/html-program-viewer/package.json index 1919de69780..6d17c449203 100644 --- a/packages/html-program-viewer/package.json +++ b/packages/html-program-viewer/package.json @@ -1,6 +1,6 @@ { "name": "@typespec/html-program-viewer", - "version": "0.56.0", + "version": "0.57.0", "author": "Microsoft Corporation", "description": "TypeSpec library for emitting an html view of the program.", "homepage": "https://typespec.io", diff --git a/packages/http-client-csharp/emitter/src/emitter.ts b/packages/http-client-csharp/emitter/src/emitter.ts index 919286f0a6f..9a4eff820c7 100644 --- a/packages/http-client-csharp/emitter/src/emitter.ts +++ b/packages/http-client-csharp/emitter/src/emitter.ts @@ -113,6 +113,8 @@ export async function $onEmit(context: EmitContext) { "generate-test-project": options["generate-test-project"] === false ? undefined : options["generate-test-project"], "use-model-reader-writer": options["use-model-reader-writer"] ?? true, + "disable-xml-docs": + options["disable-xml-docs"] === false ? undefined : options["disable-xml-docs"], }; await program.host.writeFile( diff --git a/packages/http-client-csharp/emitter/src/lib/client-model-builder.ts b/packages/http-client-csharp/emitter/src/lib/client-model-builder.ts index ddce7763ec7..3225b689fc4 100644 --- a/packages/http-client-csharp/emitter/src/lib/client-model-builder.ts +++ b/packages/http-client-csharp/emitter/src/lib/client-model-builder.ts @@ -36,8 +36,6 @@ import { InputConstant } from "../type/input-constant.js"; import { InputOperationParameterKind } from "../type/input-operation-parameter-kind.js"; import { InputOperation } from "../type/input-operation.js"; import { InputParameter } from "../type/input-parameter.js"; -import { InputPrimitiveTypeKind } from "../type/input-primitive-type-kind.js"; -import { InputTypeKind } from "../type/input-type-kind.js"; import { InputEnumType, InputModelType, InputPrimitiveType } from "../type/input-type.js"; import { RequestLocation } from "../type/request-location.js"; import { Usage } from "../type/usage.js"; @@ -79,26 +77,35 @@ export function createModelForService( const apiVersions: Set | undefined = new Set(); let defaultApiVersion: string | undefined = undefined; - const versions = getVersions(program, service.type)[1]?.getVersions(); + let versions = getVersions(program, service.type)[1] + ?.getVersions() + .map((v) => v.value); + const targetApiVersion = sdkContext.emitContext.options["api-version"]; + if ( + versions !== undefined && + targetApiVersion !== undefined && + targetApiVersion !== "all" && + targetApiVersion !== "latest" + ) { + const targetApiVersionIndex = versions.findIndex((v) => v === targetApiVersion); + versions = versions.slice(0, targetApiVersionIndex + 1); + } if (versions && versions.length > 0) { for (const ver of versions) { - apiVersions.add(ver.value); + apiVersions.add(ver); } - defaultApiVersion = versions[versions.length - 1].value; + defaultApiVersion = versions[versions.length - 1]; } const defaultApiVersionConstant: InputConstant | undefined = defaultApiVersion ? { Type: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, } as InputPrimitiveType, Value: defaultApiVersion, } : undefined; - const description = getDoc(program, serviceNamespaceType); - const servers = getServers(program, serviceNamespaceType); const namespace = getNamespaceFullName(serviceNamespaceType) || "client"; const authentication = getAuthentication(program, serviceNamespaceType); @@ -140,8 +147,33 @@ export function createModelForService( addChildClients(sdkContext.emitContext, client, clients); } + navigateModels(sdkContext, modelMap, enumMap); + + const usages = getUsages(sdkContext, convenienceOperations, modelMap); + setUsage(usages, modelMap); + setUsage(usages, enumMap); + for (const client of clients) { for (const op of client.Operations) { + /* TODO: remove this when adopt tcgc. + *set Multipart usage for models. + */ + const bodyParameter = op.Parameters.find((value) => value.Location === RequestLocation.Body); + if (bodyParameter && bodyParameter.Type && (bodyParameter.Type as InputModelType)) { + const inputModelType = bodyParameter.Type as InputModelType; + op.RequestMediaTypes?.forEach((item) => { + if (item === "multipart/form-data" && !inputModelType.Usage.includes(Usage.Multipart)) { + if (inputModelType.Usage.trim().length === 0) { + inputModelType.Usage = inputModelType.Usage.concat(Usage.Multipart); + } else { + inputModelType.Usage = inputModelType.Usage.trim() + .concat(",") + .concat(Usage.Multipart); + } + } + }); + } + const apiVersionIndex = op.Parameters.findIndex( (value: InputParameter) => value.IsApiVersion ); @@ -159,15 +191,8 @@ export function createModelForService( } } - navigateModels(sdkContext, modelMap, enumMap); - - const usages = getUsages(sdkContext, convenienceOperations, modelMap); - setUsage(usages, modelMap); - setUsage(usages, enumMap); - const clientModel = { Name: namespace, - Description: description, ApiVersions: Array.from(apiVersions.values()), Enums: Array.from(enumMap.values()), Models: Array.from(modelMap.values()), diff --git a/packages/http-client-csharp/emitter/src/lib/converter.ts b/packages/http-client-csharp/emitter/src/lib/converter.ts index 40dfc609ccc..46a254d99f9 100644 --- a/packages/http-client-csharp/emitter/src/lib/converter.ts +++ b/packages/http-client-csharp/emitter/src/lib/converter.ts @@ -19,27 +19,16 @@ import { SdkUnionType, UsageFlags, isReadOnly, - isSdkBuiltInKind, } from "@azure-tools/typespec-client-generator-core"; -import { - DateTimeKnownEncoding, - DurationKnownEncoding, - EncodeData, - IntrinsicType, - Model, - Program, - Type, - getFormat, -} from "@typespec/compiler"; +import { Model } from "@typespec/compiler"; import { InputEnumTypeValue } from "../type/input-enum-type-value.js"; -import { InputIntrinsicTypeKind } from "../type/input-intrinsic-type-kind.js"; import { InputModelProperty } from "../type/input-model-property.js"; -import { InputPrimitiveTypeKind } from "../type/input-primitive-type-kind.js"; import { InputTypeKind } from "../type/input-type-kind.js"; import { + InputDateTimeType, InputDictionaryType, + InputDurationType, InputEnumType, - InputIntrinsicType, InputListType, InputLiteralType, InputModelType, @@ -49,7 +38,6 @@ import { } from "../type/input-type.js"; import { LiteralTypeContext } from "../type/literal-type-context.js"; import { Usage } from "../type/usage.js"; -import { Logger } from "./logger.js"; import { getFullNamespaceString } from "./utils.js"; export function fromSdkType( @@ -68,34 +56,16 @@ export function fromSdkType( if (sdkType.kind === "constant") return fromSdkConstantType(sdkType, context, models, enums, literalTypeContext); if (sdkType.kind === "union") return fromUnionType(sdkType, context, models, enums); - if (sdkType.kind === "utcDateTime") return fromSdkDatetimeType(sdkType); + if (sdkType.kind === "utcDateTime" || sdkType.kind === "offsetDateTime") + return fromSdkDateTimeType(sdkType); if (sdkType.kind === "duration") return fromSdkDurationType(sdkType as SdkDurationType); - if (sdkType.kind === "bytes") return fromBytesType(sdkType as SdkBuiltInType); - if (sdkType.kind === "string") return fromStringType(context.program, sdkType); - // TODO: offsetDateTime if (sdkType.kind === "tuple") return fromTupleType(sdkType); - if (sdkType.__raw?.kind === "Scalar") return fromScalarType(sdkType); - // this happens for discriminator type, normally all other primitive types should be handled in scalar above - // TODO: can we improve the type in TCGC around discriminator - if (sdkType.__raw?.kind === "Intrinsic") return fromIntrinsicType(sdkType); - if (isSdkBuiltInKind(sdkType.kind)) return fromSdkBuiltInType(sdkType as SdkBuiltInType); - return {} as InputType; -} + // TODO -- only in operations we could have these types, considering we did not adopt getAllOperations from TCGC yet, this should be fine. + // we need to resolve these conversions when we adopt getAllOperations + if (sdkType.kind === "credential") throw new Error("Credential type is not supported yet."); + if (sdkType.kind === "endpoint") throw new Error("Endpoint type is not supported yet."); -// TODO -- this is workaround because TCGC ignore format, we need to remove this after a discussion on format. -// this function is only for the case when we get a type from a parameter, because in typespec, the parameters share the same type as the properties -export function fromSdkModelPropertyType( - propertyType: SdkModelPropertyType, - context: SdkContext, - models: Map, - enums: Map, - literalTypeContext?: LiteralTypeContext -): InputType { - // when the type is string, we need to add the format - if (propertyType.type.kind === "string") { - return fromStringType(context.program, propertyType.type, propertyType.__raw); - } - return fromSdkType(propertyType.type, context, models, enums, literalTypeContext); + return fromSdkBuiltInType(sdkType); } export function fromSdkModelType( @@ -130,56 +100,85 @@ export function fromSdkModelType( : undefined; inputModelType.InheritedDictionaryType = modelType.additionalProperties - ? ({ + ? { Kind: InputTypeKind.Dictionary, Name: InputTypeKind.Dictionary, KeyType: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, - } as InputPrimitiveType, + }, ValueType: fromSdkType(modelType.additionalProperties, context, models, enums), IsNullable: false, - } as InputDictionaryType) + } : undefined; inputModelType.Properties = modelType.properties .filter((p) => !(p as SdkBodyModelPropertyType).discriminator || !baseModelHasDiscriminator) .filter((p) => p.kind !== "header" && p.kind !== "query" && p.kind !== "path") .map((p) => - fromSdkModelProperty(p, { - ModelName: inputModelType?.Name, - Namespace: inputModelType?.Namespace, - } as LiteralTypeContext) - ); + fromSdkModelProperty( + p, + { + ModelName: inputModelType?.Name, + Namespace: inputModelType?.Namespace, + } as LiteralTypeContext, + [] + ) + ) + .flat(); } return inputModelType; function fromSdkModelProperty( propertyType: SdkModelPropertyType, - literalTypeContext: LiteralTypeContext - ): InputModelProperty { - const serializedName = - propertyType.kind === "property" - ? (propertyType as SdkBodyModelPropertyType).serializedName - : ""; - literalTypeContext.PropertyName = serializedName; - - const isRequired = - propertyType.kind === "path" || propertyType.kind === "body" ? true : !propertyType.optional; // TO-DO: SdkBodyParameter lacks of optional - const isDiscriminator = - propertyType.kind === "property" && propertyType.discriminator ? true : false; - const modelProperty: InputModelProperty = { - Name: propertyType.name, - SerializedName: serializedName, - Description: propertyType.description ?? (isDiscriminator ? "Discriminator" : ""), - Type: fromSdkType(propertyType.type, context, models, enums, literalTypeContext), - IsRequired: isRequired, - IsReadOnly: propertyType.kind === "property" && isReadOnly(propertyType), - IsDiscriminator: isDiscriminator === true ? true : undefined, // TODO: keep backward compatible to ease comparison. remove this after TCGC is merged - }; + literalTypeContext: LiteralTypeContext, + flattenedNamePrefixes: string[] + ): InputModelProperty[] { + if (propertyType.kind !== "property" || !propertyType.flatten) { + const serializedName = + propertyType.kind === "property" + ? (propertyType as SdkBodyModelPropertyType).serializedName + : ""; + literalTypeContext.PropertyName = serializedName; + + const isRequired = + propertyType.kind === "path" || propertyType.kind === "body" + ? true + : !propertyType.optional; // TO-DO: SdkBodyParameter lacks of optional + const isDiscriminator = + propertyType.kind === "property" && propertyType.discriminator ? true : false; + const modelProperty: InputModelProperty = { + Name: propertyType.name, + SerializedName: serializedName, + Description: propertyType.description ?? (isDiscriminator ? "Discriminator" : ""), + Type: fromSdkType(propertyType.type, context, models, enums, literalTypeContext), + IsRequired: isRequired, + IsReadOnly: propertyType.kind === "property" && isReadOnly(propertyType), + IsDiscriminator: isDiscriminator === true ? true : undefined, + FlattenedNames: + flattenedNamePrefixes.length > 0 + ? flattenedNamePrefixes.concat(propertyType.name) + : undefined, + }; + + return [modelProperty]; + } + + let flattenedProperties: InputModelProperty[] = []; + const modelPropertyType = propertyType as SdkBodyModelPropertyType; + const childPropertiesToFlatten = (modelPropertyType.type as SdkModelType).properties; + const newFlattenedNamePrefixes = flattenedNamePrefixes.concat(modelPropertyType.serializedName); + for (let index = 0; index < childPropertiesToFlatten.length; index++) { + flattenedProperties = flattenedProperties.concat( + fromSdkModelProperty( + childPropertiesToFlatten[index], + literalTypeContext, + newFlattenedNamePrefixes + ) + ); + } - return modelProperty; + return flattenedProperties; } } @@ -212,10 +211,10 @@ export function fromSdkEnumType( let inputEnumType = enums.get(enumName); if (inputEnumType === undefined) { const newInputEnumType: InputEnumType = { - Kind: InputTypeKind.Enum, + Kind: "enum", Name: enumName, - EnumValueType: fromScalarType(enumType.valueType).Name, - AllowedValues: enumType.values.map((v) => fromSdkEnumValueType(v)), + ValueType: fromSdkBuiltInType(enumType.valueType), + Values: enumType.values.map((v) => fromSdkEnumValueType(v)), Namespace: getFullNamespaceString( // Enum and Union have optional namespace property (enumType.__raw! as any).namespace @@ -234,328 +233,37 @@ export function fromSdkEnumType( return inputEnumType; } -function fromSdkDatetimeType(dateTimeType: SdkDatetimeType): InputPrimitiveType { - function fromDateTimeKnownEncoding(encoding: DateTimeKnownEncoding): InputPrimitiveTypeKind { - switch (encoding) { - case "rfc3339": - return InputPrimitiveTypeKind.DateTimeRFC3339; - case "rfc7231": - return InputPrimitiveTypeKind.DateTimeRFC7231; - case "unixTimestamp": - return InputPrimitiveTypeKind.DateTimeUnix; - } - } - +function fromSdkDateTimeType(dateTimeType: SdkDatetimeType): InputDateTimeType { return { - Kind: InputTypeKind.Primitive, - Name: fromDateTimeKnownEncoding(dateTimeType.encode), + Kind: dateTimeType.kind, IsNullable: dateTimeType.nullable, - } as InputPrimitiveType; + Encode: dateTimeType.encode, + WireType: fromSdkBuiltInType(dateTimeType.wireType), + }; } -function fromSdkDurationType(durationType: SdkDurationType): InputPrimitiveType { - function fromDurationKnownEncoding( - encode: DurationKnownEncoding, - wireType: SdkBuiltInType - ): InputPrimitiveTypeKind { - switch (encode) { - case "ISO8601": - return InputPrimitiveTypeKind.DurationISO8601; - case "seconds": - if (wireType.kind === "float" || wireType.kind === "float32") { - return InputPrimitiveTypeKind.DurationSecondsFloat; - } - if (wireType.kind === "float64") { - return InputPrimitiveTypeKind.DurationSecondsDouble; - } - return InputPrimitiveTypeKind.DurationSeconds; - default: - Logger.getInstance().warn( - `invalid encode '${encode}' and wireType '${wireType.kind}' for duration.` - ); - return InputPrimitiveTypeKind.DurationISO8601; - } - } - +function fromSdkDurationType(durationType: SdkDurationType): InputDurationType { return { - Kind: InputTypeKind.Primitive, - Name: fromDurationKnownEncoding(durationType.encode, durationType.wireType), + Kind: durationType.kind, IsNullable: durationType.nullable, - } as InputPrimitiveType; + Encode: durationType.encode, + WireType: fromSdkBuiltInType(durationType.wireType), + }; } // TODO: tuple is not officially supported -function fromTupleType(tupleType: SdkTupleType): InputIntrinsicType { +function fromTupleType(tupleType: SdkTupleType): InputPrimitiveType { return { - Kind: InputTypeKind.Intrinsic, - Name: InputIntrinsicTypeKind.Unknown, + Kind: "any", IsNullable: tupleType.nullable, - } as InputIntrinsicType; -} - -function fromBytesType(bytesType: SdkBuiltInType): InputPrimitiveType { - function fromBytesEncoding(encode: string): InputPrimitiveTypeKind { - switch (encode) { - case undefined: - case "base64": - return InputPrimitiveTypeKind.Bytes; - case "base64url": - return InputPrimitiveTypeKind.BytesBase64Url; - default: - Logger.getInstance().warn(`invalid encode ${encode} for bytes.`); - return InputPrimitiveTypeKind.Bytes; - } - } - - return { - Kind: InputTypeKind.Primitive, - Name: fromBytesEncoding(bytesType.encode), - IsNullable: bytesType.nullable, }; } -function fromStringType( - program: Program, - stringType: SdkType, - // we need the extra raw here because the format decorator is added to the property/parameter, but the raw in stringType is the type itself, and it does not have the format decorator we want. - // only when we get the type from a parameter, we need to pass the the parameter as raw here to get the format - // TODO -- we should remove this entirely later because TCGC ignores format in these cases, we add it now because we have old test projects, and we did not discuss the impact yet - raw?: Type -): InputPrimitiveType { - function fromStringFormat(rawStringType?: Type): InputPrimitiveTypeKind { - if (!rawStringType) return InputPrimitiveTypeKind.String; - - const format = getFormat(program, rawStringType); - switch (format) { - case "date": - // TODO: remove - return InputPrimitiveTypeKind.DateTime; - case "uri": - case "url": - return InputPrimitiveTypeKind.Uri; - case "uuid": - return InputPrimitiveTypeKind.Guid; - default: - if (format) { - Logger.getInstance().warn(`Invalid string format '${format}'`); - } - return InputPrimitiveTypeKind.String; - } - } - - raw = raw ?? stringType.__raw; - return { - Kind: InputTypeKind.Primitive, - Name: fromStringFormat(raw), - IsNullable: stringType.nullable, - }; -} - -function fromSdkBuiltInType(builtInType: SdkBuiltInType): InputType { - const builtInKind: InputPrimitiveTypeKind = mapTcgcTypeToCSharpInputTypeKind(builtInType); +function fromSdkBuiltInType(builtInType: SdkBuiltInType): InputPrimitiveType { return { - Kind: InputTypeKind.Primitive, - Name: builtInKind, + Kind: builtInType.kind, IsNullable: builtInType.nullable, - } as InputPrimitiveType; -} - -function mapTcgcTypeToCSharpInputTypeKind(type: SdkBuiltInType): InputPrimitiveTypeKind { - switch (type.kind) { - case "numeric": - return InputPrimitiveTypeKind.Float128; - case "integer": - return InputPrimitiveTypeKind.Int64; - case "safeint": - return InputPrimitiveTypeKind.SafeInt; - case "int8": - return InputPrimitiveTypeKind.SByte; - case "int32": - return InputPrimitiveTypeKind.Int32; - case "int64": - return InputPrimitiveTypeKind.Int64; - case "uint8": - return InputPrimitiveTypeKind.Byte; - case "bytes": - switch (type.encode) { - case undefined: - case "": - case "base64": - return InputPrimitiveTypeKind.Bytes; - case "base64url": - return InputPrimitiveTypeKind.BytesBase64Url; - default: - Logger.getInstance().warn(`invalid encode '${type.encode}' for bytes.`); - return InputPrimitiveTypeKind.Bytes; - } - case "float": - return InputPrimitiveTypeKind.Float64; - case "float32": - return InputPrimitiveTypeKind.Float32; - case "float64": - return InputPrimitiveTypeKind.Float64; - case "decimal": - return InputPrimitiveTypeKind.Decimal; - case "decimal128": - return InputPrimitiveTypeKind.Decimal128; - case "uuid": - return InputPrimitiveTypeKind.Guid; - case "eTag": - return InputPrimitiveTypeKind.String; - case "azureLocation": - return InputPrimitiveTypeKind.AzureLocation; - case "string": - return InputPrimitiveTypeKind.String; - case "guid": // TODO: is this the same as uuid? - return InputPrimitiveTypeKind.Guid; - case "uri": - case "url": - return InputPrimitiveTypeKind.Uri; - case "boolean": - return InputPrimitiveTypeKind.Boolean; - case "plainDate": - return InputPrimitiveTypeKind.Date; - case "plainTime": - return InputPrimitiveTypeKind.Time; - case "any": - return InputPrimitiveTypeKind.Object; - case "int16": // TODO: add support for those types - case "uint16": - case "uint32": - case "uint64": - case "ipV4Address": - case "ipV6Address": - case "password": - case "armId": - case "ipAddress": - default: - throw new Error(`Unsupported built-in type kind '${type.kind}'`); - } -} - -function fromScalarType(scalarType: SdkType): InputPrimitiveType { - return { - Kind: InputTypeKind.Primitive, - Name: getCSharpInputTypeKindByPrimitiveModelName( - scalarType.kind, - scalarType.kind, - undefined // To-DO: encode not compatible - ), - IsNullable: scalarType.nullable, - }; - - function getCSharpInputTypeKindByPrimitiveModelName( - name: string, - format?: string, - encode?: EncodeData - ): InputPrimitiveTypeKind { - switch (name) { - case "bytes": - switch (encode?.encoding) { - case undefined: - case "base64": - return InputPrimitiveTypeKind.Bytes; - case "base64url": - return InputPrimitiveTypeKind.BytesBase64Url; - default: - Logger.getInstance().warn(`invalid encode ${encode?.encoding} for bytes.`); - return InputPrimitiveTypeKind.Bytes; - } - case "int8": - return InputPrimitiveTypeKind.SByte; - case "uint8": - return InputPrimitiveTypeKind.Byte; - case "int32": - return InputPrimitiveTypeKind.Int32; - case "int64": - return InputPrimitiveTypeKind.Int64; - case "integer": - return InputPrimitiveTypeKind.Int64; - case "safeint": - return InputPrimitiveTypeKind.SafeInt; - case "float32": - return InputPrimitiveTypeKind.Float32; - case "float64": - return InputPrimitiveTypeKind.Float64; - case "decimal": - return InputPrimitiveTypeKind.Decimal; - case "decimal128": - return InputPrimitiveTypeKind.Decimal128; - case "uri": - case "url": - return InputPrimitiveTypeKind.Uri; - case "uuid": - return InputPrimitiveTypeKind.Guid; - case "eTag": - return InputPrimitiveTypeKind.String; - case "string": - switch (format?.toLowerCase()) { - case "date": - return InputPrimitiveTypeKind.DateTime; - case "uri": - case "url": - return InputPrimitiveTypeKind.Uri; - case "uuid": - return InputPrimitiveTypeKind.Guid; - default: - if (format) { - Logger.getInstance().warn(`invalid format ${format}`); - } - return InputPrimitiveTypeKind.String; - } - case "boolean": - return InputPrimitiveTypeKind.Boolean; - case "date": - return InputPrimitiveTypeKind.Date; - case "plainDate": - return InputPrimitiveTypeKind.Date; - case "plainTime": - return InputPrimitiveTypeKind.Time; - case "datetime": - case "utcDateTime": - switch (encode?.encoding) { - case undefined: - return InputPrimitiveTypeKind.DateTime; - case "rfc3339": - return InputPrimitiveTypeKind.DateTimeRFC3339; - case "rfc7231": - return InputPrimitiveTypeKind.DateTimeRFC7231; - case "unixTimestamp": - return InputPrimitiveTypeKind.DateTimeUnix; - default: - Logger.getInstance().warn(`invalid encode ${encode?.encoding} for date time.`); - return InputPrimitiveTypeKind.DateTime; - } - case "time": - return InputPrimitiveTypeKind.Time; - case "duration": - switch (encode?.encoding) { - case undefined: - case "ISO8601": - return InputPrimitiveTypeKind.DurationISO8601; - case "seconds": - if (encode.type?.name === "float" || encode.type?.name === "float32") { - return InputPrimitiveTypeKind.DurationSecondsFloat; - } else { - return InputPrimitiveTypeKind.DurationSeconds; - } - default: - Logger.getInstance().warn(`invalid encode ${encode?.encoding} for duration.`); - return InputPrimitiveTypeKind.DurationISO8601; - } - case "azureLocation": - return InputPrimitiveTypeKind.AzureLocation; - default: - return InputPrimitiveTypeKind.Object; - } - } -} - -function fromIntrinsicType(scalarType: SdkType): InputIntrinsicType { - return { - Kind: InputTypeKind.Intrinsic, - Name: getCSharpInputTypeKindByIntrinsic(scalarType.__raw! as IntrinsicType), - IsNullable: false, + Encode: builtInType.encode !== builtInType.kind ? builtInType.encode : undefined, // In TCGC this is required, and when there is no encoding, it just has the same value as kind, we could remove this when TCGC decides to simplify }; } @@ -565,20 +273,18 @@ function fromUnionType( models: Map, enums: Map ): InputUnionType | InputType { - const itemTypes: InputType[] = []; + const variantTypes: InputType[] = []; for (const value of union.values) { - const inputType = fromSdkType(value, context, models, enums); - itemTypes.push(inputType); + const variantType = fromSdkType(value, context, models, enums); + variantTypes.push(variantType); } - return itemTypes.length > 1 - ? { - Kind: InputTypeKind.Union, - Name: InputTypeKind.Union, - UnionItemTypes: itemTypes, - IsNullable: false, - } - : itemTypes[0]; + return { + Kind: "union", + Name: union.name, + VariantTypes: variantTypes, + IsNullable: false, + }; } function fromSdkConstantType( @@ -589,9 +295,8 @@ function fromSdkConstantType( literalTypeContext?: LiteralTypeContext ): InputLiteralType { return { - Kind: InputTypeKind.Literal, - Name: InputTypeKind.Literal, - LiteralValueType: + Kind: constantType.kind, + ValueType: constantType.valueType.kind === "boolean" || literalTypeContext === undefined ? fromSdkBuiltInType(constantType.valueType) : // TODO: this might change in the near future @@ -609,14 +314,6 @@ function fromSdkConstantType( // otherwise we need to wrap this into an extensible enum // we use the model name followed by the property name as the enum name to ensure it is unique const enumName = `${literalTypeContext.ModelName}_${literalTypeContext.PropertyName}`; - const valueType = fromSdkType( - constantType.valueType, - context, - models, - enums, - literalTypeContext - ) as InputPrimitiveType; - const enumValueType = valueType.Name; const enumValueName = constantType.value === null ? "Null" : constantType.value.toString(); const allowValues: InputEnumTypeValue[] = [ { @@ -626,10 +323,10 @@ function fromSdkConstantType( }, ]; const enumType: InputEnumType = { - Kind: InputTypeKind.Enum, + Kind: "enum", Name: enumName, - EnumValueType: enumValueType, //EnumValueType and AllowedValues should be the first field after id and name, so that it can be corrected serialized. - AllowedValues: allowValues, + ValueType: fromSdkBuiltInType(constantType.valueType), + Values: allowValues, Namespace: literalTypeContext.Namespace, Accessibility: undefined, Deprecated: undefined, @@ -650,9 +347,8 @@ function fromSdkEnumValueTypeToConstantType( literalTypeContext?: LiteralTypeContext ): InputLiteralType { return { - Kind: InputTypeKind.Literal, - Name: InputTypeKind.Literal, - LiteralValueType: + Kind: "constant", + ValueType: enumValueType.valueType.kind === "boolean" || literalTypeContext === undefined ? fromSdkBuiltInType(enumValueType.valueType as SdkBuiltInType) // TODO: TCGC fix : fromSdkEnumType(enumValueType.enumType, context, enums), @@ -704,20 +400,3 @@ function fromUsageFlags(usage: UsageFlags): Usage { else if (usage === (UsageFlags.Input | UsageFlags.Output)) return Usage.RoundTrip; else return Usage.None; } - -function getCSharpInputTypeKindByIntrinsic(type: IntrinsicType): InputIntrinsicTypeKind { - switch (type.name) { - case "ErrorType": - return InputIntrinsicTypeKind.Error; - case "void": - return InputIntrinsicTypeKind.Void; - case "never": - return InputIntrinsicTypeKind.Never; - case "unknown": - return InputIntrinsicTypeKind.Unknown; - case "null": - return InputIntrinsicTypeKind.Null; - default: - throw new Error(`Unsupported intrinsic type name '${type.name}'`); - } -} diff --git a/packages/http-client-csharp/emitter/src/lib/model.ts b/packages/http-client-csharp/emitter/src/lib/model.ts index 1f8aeb6f914..db5c7e13b3e 100644 --- a/packages/http-client-csharp/emitter/src/lib/model.ts +++ b/packages/http-client-csharp/emitter/src/lib/model.ts @@ -6,7 +6,6 @@ import { SdkContext, getAllModels, getClientType, - getSdkModelPropertyType, } from "@azure-tools/typespec-client-generator-core"; import { Model, @@ -15,7 +14,6 @@ import { Type, UsageFlags, getEffectiveModelType, - ignoreDiagnostics, isArrayModelType, isRecordModelType, resolveUsages, @@ -36,12 +34,7 @@ import { isInputLiteralType, } from "../type/input-type.js"; import { LiteralTypeContext } from "../type/literal-type-context.js"; -import { - fromSdkEnumType, - fromSdkModelPropertyType, - fromSdkModelType, - fromSdkType, -} from "./converter.js"; +import { fromSdkEnumType, fromSdkModelType, fromSdkType } from "./converter.js"; import { Logger } from "./logger.js"; import { capitalize, getTypeName } from "./utils.js"; @@ -105,14 +98,6 @@ export function getInputType( ): InputType { Logger.getInstance().debug(`getInputType for kind: ${type.kind}`); - // TODO -- we might could remove this workaround when we adopt getAllOperations - // or when we decide not to honor the `@format` decorators on parameters - // this is specifically dealing with the case of an operation parameter - if (type.kind === "ModelProperty") { - const propertyType = ignoreDiagnostics(getSdkModelPropertyType(context, type, operation)); - return fromSdkModelPropertyType(propertyType, context, models, enums, literalTypeContext); - } - const sdkType = getClientType(context, type, operation); return fromSdkType(sdkType, context, models, enums, literalTypeContext); } @@ -244,7 +229,7 @@ export function getUsages( if (!isInputLiteralType(type)) continue; // now type should be a literal type // find its corresponding enum type - const literalValueType = type.LiteralValueType; + const literalValueType = type.ValueType; if (!isInputEnumType(literalValueType)) continue; // now literalValueType should be an enum type // apply the usage on this model to the usagesMap diff --git a/packages/http-client-csharp/emitter/src/lib/operation.ts b/packages/http-client-csharp/emitter/src/lib/operation.ts index 78b6fa17286..37ad8229bd6 100644 --- a/packages/http-client-csharp/emitter/src/lib/operation.ts +++ b/packages/http-client-csharp/emitter/src/lib/operation.ts @@ -29,7 +29,6 @@ import { InputConstant } from "../type/input-constant.js"; import { InputOperationParameterKind } from "../type/input-operation-parameter-kind.js"; import { InputOperation } from "../type/input-operation.js"; import { InputParameter } from "../type/input-parameter.js"; -import { InputTypeKind } from "../type/input-type-kind.js"; import { InputEnumType, InputListType, @@ -133,15 +132,15 @@ export function loadOperation( if (isInputLiteralType(contentTypeParameter.Type)) { mediaTypes.push(contentTypeParameter.DefaultValue?.Value); } else if (isInputUnionType(contentTypeParameter.Type)) { - const mediaTypeValues = contentTypeParameter.Type.UnionItemTypes.map((item) => - isInputLiteralType(item) ? item.Value : undefined - ); - if (mediaTypeValues.some((item) => item === undefined)) { - throw "Media type of content type should be string."; + for (const unionItem of contentTypeParameter.Type.VariantTypes) { + if (isInputLiteralType(unionItem)) { + mediaTypes.push(unionItem.Value as string); + } else { + throw "Media type of content type should be string."; + } } - mediaTypes.push(...mediaTypeValues); } else if (isInputEnumType(contentTypeParameter.Type)) { - const mediaTypeValues = contentTypeParameter.Type.AllowedValues.map((value) => value.Value); + const mediaTypeValues = contentTypeParameter.Type.Values.map((value) => value.Value); if (mediaTypeValues.some((item) => item === undefined)) { throw "Media type of content type should be string."; } @@ -221,7 +220,7 @@ export function loadOperation( const isContentType: boolean = requestLocation === RequestLocation.Header && name.toLowerCase() === "content-type"; const kind: InputOperationParameterKind = - isContentType || inputType.Kind === InputTypeKind.Literal + isContentType || inputType.Kind === "constant" ? InputOperationParameterKind.Constant : isApiVer ? defaultValue diff --git a/packages/http-client-csharp/emitter/src/lib/typespec-server.ts b/packages/http-client-csharp/emitter/src/lib/typespec-server.ts index 5a1025386d4..37983625fbf 100644 --- a/packages/http-client-csharp/emitter/src/lib/typespec-server.ts +++ b/packages/http-client-csharp/emitter/src/lib/typespec-server.ts @@ -2,14 +2,12 @@ // Licensed under the MIT License. See License.txt in the project root for license information. import { SdkContext } from "@azure-tools/typespec-client-generator-core"; -import { getDoc, Type } from "@typespec/compiler"; +import { getDoc } from "@typespec/compiler"; import { HttpServer } from "@typespec/http"; import { NetEmitterOptions } from "../options.js"; import { InputConstant } from "../type/input-constant.js"; import { InputOperationParameterKind } from "../type/input-operation-parameter-kind.js"; import { InputParameter } from "../type/input-parameter.js"; -import { InputPrimitiveTypeKind } from "../type/input-primitive-type-kind.js"; -import { InputTypeKind } from "../type/input-type-kind.js"; import { InputEnumType, InputModelType, @@ -17,7 +15,7 @@ import { InputType, } from "../type/input-type.js"; import { RequestLocation } from "../type/request-location.js"; -import { getInputType } from "./model.js"; +import { getDefaultValue, getInputType } from "./model.js"; export interface TypeSpecServer { url: string; @@ -25,21 +23,6 @@ export interface TypeSpecServer { parameters: InputParameter[]; } -function getDefaultValue(type: Type): any { - switch (type.kind) { - case "String": - return type.value; - case "Number": - return type.value; - case "Boolean": - return type.value; - case "Tuple": - return type.values.map(getDefaultValue); - default: - return undefined; - } -} - export function resolveServers( context: SdkContext, servers: HttpServer[], @@ -56,8 +39,7 @@ export function resolveServers( const value = prop.default ? getDefaultValue(prop.default) : ""; const inputType: InputType = isEndpoint ? ({ - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.Uri, + Kind: "uri", IsNullable: false, } as InputPrimitiveType) : getInputType(context, prop, models, enums); @@ -94,8 +76,7 @@ export function resolveServers( NameInRequest: "host", Description: server.description, Type: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, } as InputPrimitiveType, Location: RequestLocation.Uri, @@ -109,8 +90,7 @@ export function resolveServers( Kind: InputOperationParameterKind.Client, DefaultValue: { Type: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, } as InputPrimitiveType, Value: server.url, diff --git a/packages/http-client-csharp/emitter/src/lib/utils.ts b/packages/http-client-csharp/emitter/src/lib/utils.ts index 8296021a310..eab4d42dec9 100644 --- a/packages/http-client-csharp/emitter/src/lib/utils.ts +++ b/packages/http-client-csharp/emitter/src/lib/utils.ts @@ -15,8 +15,6 @@ import { import { InputConstant } from "../type/input-constant.js"; import { InputOperationParameterKind } from "../type/input-operation-parameter-kind.js"; import { InputParameter } from "../type/input-parameter.js"; -import { InputPrimitiveTypeKind } from "../type/input-primitive-type-kind.js"; -import { InputTypeKind } from "../type/input-type-kind.js"; import { InputPrimitiveType, InputType } from "../type/input-type.js"; import { RequestLocation } from "../type/request-location.js"; @@ -70,8 +68,7 @@ export function createContentTypeOrAcceptParameter( ): InputParameter { const isContentType: boolean = nameInRequest.toLowerCase() === "content-type"; const inputType: InputType = { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, } as InputPrimitiveType; return { diff --git a/packages/http-client-csharp/emitter/src/options.ts b/packages/http-client-csharp/emitter/src/options.ts index b29341f3f59..08275136634 100644 --- a/packages/http-client-csharp/emitter/src/options.ts +++ b/packages/http-client-csharp/emitter/src/options.ts @@ -30,6 +30,7 @@ export type NetEmitterOptions = { "generate-sample-project"?: boolean; "generate-test-project"?: boolean; "use-model-reader-writer"?: boolean; + "disable-xml-docs"?: boolean; } & SdkEmitterOptions; export const NetEmitterOptionsSchema: JSONSchemaType = { @@ -101,6 +102,7 @@ export const NetEmitterOptionsSchema: JSONSchemaType = { default: false, }, "use-model-reader-writer": { type: "boolean", nullable: true }, + "disable-xml-docs": { type: "boolean", nullable: true }, }, required: [], }; diff --git a/packages/http-client-csharp/emitter/src/type/code-model.ts b/packages/http-client-csharp/emitter/src/type/code-model.ts index 99075d5eeab..b2efbf54ec9 100644 --- a/packages/http-client-csharp/emitter/src/type/code-model.ts +++ b/packages/http-client-csharp/emitter/src/type/code-model.ts @@ -7,7 +7,6 @@ import { InputEnumType, InputModelType } from "./input-type.js"; export interface CodeModel { Name: string; - Description?: string; ApiVersions: string[]; Enums: InputEnumType[]; Models: InputModelType[]; diff --git a/packages/http-client-csharp/emitter/src/type/configuration.ts b/packages/http-client-csharp/emitter/src/type/configuration.ts index 93293a94749..1cf71260a19 100644 --- a/packages/http-client-csharp/emitter/src/type/configuration.ts +++ b/packages/http-client-csharp/emitter/src/type/configuration.ts @@ -19,4 +19,5 @@ export interface Configuration { "generate-sample-project"?: boolean; "generate-test-project"?: boolean; "use-model-reader-writer"?: boolean; + "disable-xml-docs"?: boolean; } diff --git a/packages/http-client-csharp/emitter/src/type/input-intrinsic-type-kind.ts b/packages/http-client-csharp/emitter/src/type/input-intrinsic-type-kind.ts deleted file mode 100644 index d7f54ace2c4..00000000000 --- a/packages/http-client-csharp/emitter/src/type/input-intrinsic-type-kind.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -export enum InputIntrinsicTypeKind { - Error = "ErrorType", - Void = "void", - Never = "never", - Unknown = "unknown", - Null = "null", -} diff --git a/packages/http-client-csharp/emitter/src/type/input-model-property.ts b/packages/http-client-csharp/emitter/src/type/input-model-property.ts index a5eac4c5ca2..e23d18c58d7 100644 --- a/packages/http-client-csharp/emitter/src/type/input-model-property.ts +++ b/packages/http-client-csharp/emitter/src/type/input-model-property.ts @@ -11,4 +11,5 @@ export interface InputModelProperty { IsRequired: boolean; IsReadOnly: boolean; IsDiscriminator?: boolean; + FlattenedNames?: string[]; } diff --git a/packages/http-client-csharp/emitter/src/type/input-primitive-type-kind.ts b/packages/http-client-csharp/emitter/src/type/input-primitive-type-kind.ts deleted file mode 100644 index 7d122b9dc17..00000000000 --- a/packages/http-client-csharp/emitter/src/type/input-primitive-type-kind.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -// TODO: clean up primitive types, separate types info from encoding info -// https://github.com/Azure/autorest.csharp/issues/4681 -export enum InputPrimitiveTypeKind { - AzureLocation = "AzureLocation", - Boolean = "Boolean", - BinaryData = "BinaryData", - Bytes = "Bytes", - BytesBase64Url = "BytesBase64Url", - ContentType = "ContentType", - Date = "Date", - DateTime = "DateTime", - DateTimeISO8601 = "DateTimeISO8601", - DateTimeRFC1123 = "DateTimeRFC1123", - DateTimeRFC3339 = "DateTimeRFC3339", - DateTimeRFC7231 = "DateTimeRFC7231", - DateTimeUnix = "DateTimeUnix", - Decimal = "Decimal", - Decimal128 = "Decimal128", - DurationISO8601 = "DurationISO8601", - DurationConstant = "DurationConstant", - DurationSeconds = "DurationSeconds", - DurationSecondsFloat = "DurationSecondsFloat", - DurationSecondsDouble = "DurationSecondsDouble", - ETag = "Etag", - Float32 = "Float32", - Float64 = "Float64", - Float128 = "Float128", - Guid = "Guid", - Int32 = "Int32", - Int64 = "Int64", - SafeInt = "SafeInt", - IPAddress = "IPAddress", - Object = "Object", - RequestMethod = "RequestMethod", - ResourceIdentifier = "ResourceIdentifier", - ResourceType = "ResourceType", - Stream = "Stream", - String = "String", - Time = "Time", - Uri = "Uri", - Enum = "Enum", - SByte = "SByte", //int8 - Byte = "Byte", //uint8 -} diff --git a/packages/http-client-csharp/emitter/src/type/input-type-kind.ts b/packages/http-client-csharp/emitter/src/type/input-type-kind.ts index f36d1c77a32..6386a2087b4 100644 --- a/packages/http-client-csharp/emitter/src/type/input-type-kind.ts +++ b/packages/http-client-csharp/emitter/src/type/input-type-kind.ts @@ -2,11 +2,7 @@ // Licensed under the MIT License. See License.txt in the project root for license information. export enum InputTypeKind { - Primitive = "Primitive", - Literal = "Literal", - Union = "Union", Model = "Model", - Enum = "Enum", Array = "Array", Dictionary = "Dictionary", Intrinsic = "Intrinsic", diff --git a/packages/http-client-csharp/emitter/src/type/input-type.ts b/packages/http-client-csharp/emitter/src/type/input-type.ts index fa0aa2a128c..e7a82939d7c 100644 --- a/packages/http-client-csharp/emitter/src/type/input-type.ts +++ b/packages/http-client-csharp/emitter/src/type/input-type.ts @@ -1,45 +1,77 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +import { SdkBuiltInKinds } from "@azure-tools/typespec-client-generator-core"; +import { DateTimeKnownEncoding, DurationKnownEncoding } from "@typespec/compiler"; import { InputEnumTypeValue } from "./input-enum-type-value.js"; -import { InputIntrinsicTypeKind } from "./input-intrinsic-type-kind.js"; import { InputModelProperty } from "./input-model-property.js"; -import { InputPrimitiveTypeKind } from "./input-primitive-type-kind.js"; import { InputTypeKind } from "./input-type-kind.js"; -export interface InputType { - Name: string; - Kind: InputTypeKind; +interface InputTypeBase { + Kind: string; IsNullable: boolean; + Description?: string; } -export interface InputPrimitiveType extends InputType { - Name: InputPrimitiveTypeKind; +export type InputType = + | InputPrimitiveType + | InputDateTimeType + | InputDurationType + | InputLiteralType + | InputUnionType + | InputModelType + | InputEnumType + | InputListType + | InputDictionaryType; + +export interface InputPrimitiveType extends InputTypeBase { + Kind: SdkBuiltInKinds; + Encode?: string; // In TCGC this is required, and when there is no encoding, it just has the same value as kind } -export interface InputLiteralType extends InputType { - Kind: InputTypeKind.Literal; - Name: InputTypeKind.Literal; // literal type does not really have a name right now, we just use its kind - LiteralValueType: InputType; - Value: any; +export interface InputLiteralType extends InputTypeBase { + Kind: "constant"; + ValueType: InputPrimitiveType | InputEnumType; // this has to be inconsistent because currently we have possibility of having an enum underlying the literal type + Value: string | number | boolean | null; } export function isInputLiteralType(type: InputType): type is InputLiteralType { - return type.Kind === InputTypeKind.Literal; + return type.Kind === "constant"; +} + +export type InputDateTimeType = InputUtcDateTimeType | InputOffsetDateTimeType; + +interface InputDateTimeTypeBase extends InputTypeBase { + Encode: DateTimeKnownEncoding; + WireType: InputPrimitiveType; +} + +export interface InputUtcDateTimeType extends InputDateTimeTypeBase { + Kind: "utcDateTime"; +} + +export interface InputOffsetDateTimeType extends InputDateTimeTypeBase { + Kind: "offsetDateTime"; } -export interface InputUnionType extends InputType { - Kind: InputTypeKind.Union; - Name: InputTypeKind.Union; // union type does not really have a name right now, we just use its kind - UnionItemTypes: InputType[]; +export interface InputDurationType extends InputTypeBase { + Kind: "duration"; + Encode: DurationKnownEncoding; + WireType: InputPrimitiveType; +} + +export interface InputUnionType extends InputTypeBase { + Kind: "union"; + Name: string; + VariantTypes: InputType[]; } export function isInputUnionType(type: InputType): type is InputUnionType { - return type.Kind === InputTypeKind.Union; + return type.Kind === "union"; } -export interface InputModelType extends InputType { - Kind: InputTypeKind.Model; +export interface InputModelType extends InputTypeBase { + Kind: InputTypeKind.Model; // TODO -- will change to TCGC value in future refactor Name: string; Namespace?: string; Accessibility?: string; @@ -58,25 +90,24 @@ export function isInputModelType(type: InputType): type is InputModelType { return type.Kind === InputTypeKind.Model; } -export interface InputEnumType extends InputType { - Kind: InputTypeKind.Enum; +export interface InputEnumType extends InputTypeBase { + Kind: "enum"; Name: string; - EnumValueType: string; - AllowedValues: InputEnumTypeValue[]; + ValueType: InputPrimitiveType; + Values: InputEnumTypeValue[]; Namespace?: string; Accessibility?: string; Deprecated?: string; - Description?: string; IsExtensible: boolean; Usage: string; } export function isInputEnumType(type: InputType): type is InputEnumType { - return type.Kind === InputTypeKind.Enum; + return type.Kind === "enum"; } -export interface InputListType extends InputType { - Kind: InputTypeKind.Array; +export interface InputListType extends InputTypeBase { + Kind: InputTypeKind.Array; // TODO -- will change to TCGC value in future refactor Name: InputTypeKind.Array; // array type does not really have a name right now, we just use its kind ElementType: InputType; } @@ -85,8 +116,8 @@ export function isInputListType(type: InputType): type is InputListType { return type.Kind === InputTypeKind.Array; } -export interface InputDictionaryType extends InputType { - Kind: InputTypeKind.Dictionary; +export interface InputDictionaryType extends InputTypeBase { + Kind: InputTypeKind.Dictionary; // TODO -- will change to TCGC value in future refactor Name: InputTypeKind.Dictionary; // dictionary type does not really have a name right now, we just use its kind KeyType: InputType; ValueType: InputType; @@ -95,13 +126,3 @@ export interface InputDictionaryType extends InputType { export function isInputDictionaryType(type: InputType): type is InputDictionaryType { return type.Kind === InputTypeKind.Dictionary; } - -export interface InputIntrinsicType extends InputType { - Kind: InputTypeKind.Intrinsic; - Name: InputIntrinsicTypeKind; - IsNullable: false; -} - -export function isInputIntrinsicType(type: InputType): type is InputIntrinsicType { - return type.Kind === InputTypeKind.Intrinsic; -} diff --git a/packages/http-client-csharp/emitter/src/type/usage.ts b/packages/http-client-csharp/emitter/src/type/usage.ts index b32d287fdaa..ee9209f2f08 100644 --- a/packages/http-client-csharp/emitter/src/type/usage.ts +++ b/packages/http-client-csharp/emitter/src/type/usage.ts @@ -6,4 +6,5 @@ export enum Usage { Input = "Input", Output = "Output", RoundTrip = "RoundTrip", + Multipart = "Multipart", } diff --git a/packages/http-client-csharp/emitter/test/Unit/encode.test.ts b/packages/http-client-csharp/emitter/test/Unit/encode.test.ts index 046af339adf..36fccaf28a7 100644 --- a/packages/http-client-csharp/emitter/test/Unit/encode.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/encode.test.ts @@ -3,9 +3,7 @@ import { getAllHttpServices } from "@typespec/http"; import assert, { deepStrictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { loadOperation } from "../../src/lib/operation.js"; -import { InputPrimitiveTypeKind } from "../../src/type/input-primitive-type-kind.js"; -import { InputTypeKind } from "../../src/type/input-type-kind.js"; -import { InputEnumType, InputModelType, InputPrimitiveType } from "../../src/type/input-type.js"; +import { InputDurationType, InputEnumType, InputModelType } from "../../src/type/input-type.js"; import { createEmitterContext, createEmitterTestHost, @@ -48,10 +46,15 @@ describe("Test encode duration", () => { ); deepStrictEqual( { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.DurationISO8601, + Kind: "duration", + Encode: "ISO8601", + WireType: { + Kind: "string", + IsNullable: false, + Encode: undefined, + }, IsNullable: false, - } as InputPrimitiveType, + } as InputDurationType, operation.Parameters[0].Type ); }); @@ -83,10 +86,15 @@ describe("Test encode duration", () => { ); deepStrictEqual( { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.DurationSeconds, + Kind: "duration", + Encode: "seconds", + WireType: { + Kind: "int32", + IsNullable: false, + Encode: undefined, + }, IsNullable: false, - } as InputPrimitiveType, + } as InputDurationType, operation.Parameters[0].Type ); }); @@ -96,7 +104,7 @@ describe("Test encode duration", () => { ` op test( @query - @encode(DurationKnownEncoding.seconds, float) + @encode(DurationKnownEncoding.seconds, float32) input: duration ): NoContentResponse; `, @@ -118,10 +126,15 @@ describe("Test encode duration", () => { ); deepStrictEqual( { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.DurationSecondsFloat, + Kind: "duration", + Encode: "seconds", + WireType: { + Kind: "float32", + IsNullable: false, + Encode: undefined, + }, IsNullable: false, - } as InputPrimitiveType, + } as InputDurationType, operation.Parameters[0].Type ); }); @@ -147,10 +160,15 @@ describe("Test encode duration", () => { assert(durationProperty !== undefined); deepStrictEqual( { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.DurationISO8601, + Kind: "duration", + Encode: "ISO8601", + WireType: { + Kind: "string", + IsNullable: false, + Encode: undefined, + }, IsNullable: false, - } as InputPrimitiveType, + } as InputDurationType, durationProperty.Properties[0].Type ); }); @@ -176,20 +194,25 @@ describe("Test encode duration", () => { assert(durationProperty !== undefined); deepStrictEqual( { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.DurationSeconds, + Kind: "duration", + Encode: "seconds", + WireType: { + Kind: "int32", + IsNullable: false, + Encode: undefined, + }, IsNullable: false, - }, + } as InputDurationType, durationProperty.Properties[0].Type ); }); - it("encode seconds float on duration model property", async () => { + it("encode seconds float32 on duration model property", async () => { const program = await typeSpecCompile( ` @doc("This is a model.") model FloatSecondsDurationProperty { - @encode(DurationKnownEncoding.seconds, float) + @encode(DurationKnownEncoding.seconds, float32) value: duration; } `, @@ -205,10 +228,15 @@ describe("Test encode duration", () => { assert(durationProperty !== undefined); deepStrictEqual( { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.DurationSecondsFloat, + Kind: "duration", + Encode: "seconds", + WireType: { + Kind: "float32", + IsNullable: false, + Encode: undefined, + }, IsNullable: false, - }, + } as InputDurationType, durationProperty.Properties[0].Type ); }); diff --git a/packages/http-client-csharp/emitter/test/Unit/model-type.test.ts b/packages/http-client-csharp/emitter/test/Unit/model-type.test.ts index 26c0b820175..ae3d31d306f 100644 --- a/packages/http-client-csharp/emitter/test/Unit/model-type.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/model-type.test.ts @@ -2,9 +2,7 @@ import { TestHost } from "@typespec/compiler/testing"; import assert, { deepStrictEqual, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { createModel } from "../../src/lib/client-model-builder.js"; -import { InputIntrinsicTypeKind } from "../../src/type/input-intrinsic-type-kind.js"; import { InputModelProperty } from "../../src/type/input-model-property.js"; -import { InputPrimitiveTypeKind } from "../../src/type/input-primitive-type-kind.js"; import { InputTypeKind } from "../../src/type/input-type-kind.js"; import { InputDictionaryType } from "../../src/type/input-type.js"; import { @@ -70,14 +68,15 @@ op test(@body input: Pet): Pet; Name: "kind", SerializedName: "kind", Type: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, + Encode: undefined, }, IsRequired: true, IsReadOnly: false, IsDiscriminator: true, Description: "Discriminator", + FlattenedNames: undefined, } as InputModelProperty, discriminatorProperty ); @@ -162,14 +161,18 @@ op test(@body input: Pet): Pet; SerializedName: "kind", Description: "The kind of the pet", Type: { - Kind: InputTypeKind.Enum, + Kind: "enum", Name: "PetKind", Namespace: "Azure.Csharp.Testing", Description: "The pet kind", Accessibility: undefined, Deprecated: undefined, - EnumValueType: "String", - AllowedValues: [ + ValueType: { + Kind: "string", + IsNullable: false, + Encode: undefined, + }, + Values: [ { Name: "Cat", Value: "Cat", @@ -188,6 +191,7 @@ op test(@body input: Pet): Pet; IsRequired: true, IsReadOnly: false, IsDiscriminator: true, + FlattenedNames: undefined, } as InputModelProperty, discriminatorProperty ); @@ -286,14 +290,18 @@ op test(@body input: Pet): Pet; SerializedName: "kind", Description: "The kind of the pet", Type: { - Kind: InputTypeKind.Enum, + Kind: "enum", Name: "PetKind", Namespace: "Azure.Csharp.Testing", Accessibility: undefined, Deprecated: undefined, Description: "The pet kind", - EnumValueType: "String", - AllowedValues: [ + ValueType: { + Kind: "string", + IsNullable: false, + Encode: undefined, + }, + Values: [ { Name: "Cat", Value: "cat", @@ -312,6 +320,7 @@ op test(@body input: Pet): Pet; IsRequired: true, IsReadOnly: false, IsDiscriminator: true, + FlattenedNames: undefined, } as InputModelProperty, discriminatorProperty ); @@ -442,14 +451,13 @@ op op5(@body body: ExtendsFooArray): ExtendsFooArray; Name: InputTypeKind.Dictionary, IsNullable: false, KeyType: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, }, ValueType: { - Kind: InputTypeKind.Intrinsic, - Name: InputIntrinsicTypeKind.Unknown, + Kind: "any", IsNullable: false, + Encode: undefined, }, } as InputDictionaryType, extendsUnknownModel.InheritedDictionaryType @@ -460,14 +468,13 @@ op op5(@body body: ExtendsFooArray): ExtendsFooArray; Name: InputTypeKind.Dictionary, IsNullable: false, KeyType: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, }, ValueType: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, + Encode: undefined, }, } as InputDictionaryType, extendsStringModel.InheritedDictionaryType @@ -478,14 +485,13 @@ op op5(@body body: ExtendsFooArray): ExtendsFooArray; Name: InputTypeKind.Dictionary, IsNullable: false, KeyType: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, }, ValueType: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.Int32, + Kind: "int32", IsNullable: false, + Encode: undefined, }, } as InputDictionaryType, extendsInt32Model.InheritedDictionaryType @@ -496,8 +502,7 @@ op op5(@body body: ExtendsFooArray): ExtendsFooArray; Name: InputTypeKind.Dictionary, IsNullable: false, KeyType: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, }, ValueType: fooModel, @@ -510,8 +515,7 @@ op op5(@body body: ExtendsFooArray): ExtendsFooArray; Name: InputTypeKind.Dictionary, IsNullable: false, KeyType: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, }, ValueType: { @@ -612,14 +616,13 @@ op op5(@body body: IsFooArray): IsFooArray; Name: InputTypeKind.Dictionary, IsNullable: false, KeyType: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, }, ValueType: { - Kind: InputTypeKind.Intrinsic, - Name: InputIntrinsicTypeKind.Unknown, + Kind: "any", IsNullable: false, + Encode: undefined, }, } as InputDictionaryType, isUnknownModel.InheritedDictionaryType @@ -630,14 +633,13 @@ op op5(@body body: IsFooArray): IsFooArray; Name: InputTypeKind.Dictionary, IsNullable: false, KeyType: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, }, ValueType: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, + Encode: undefined, }, } as InputDictionaryType, isStringModel.InheritedDictionaryType @@ -648,14 +650,13 @@ op op5(@body body: IsFooArray): IsFooArray; Name: InputTypeKind.Dictionary, IsNullable: false, KeyType: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, }, ValueType: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.Int32, + Kind: "int32", IsNullable: false, + Encode: undefined, }, } as InputDictionaryType, isInt32Model.InheritedDictionaryType @@ -666,8 +667,7 @@ op op5(@body body: IsFooArray): IsFooArray; Name: InputTypeKind.Dictionary, IsNullable: false, KeyType: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, }, ValueType: fooModel, @@ -680,8 +680,7 @@ op op5(@body body: IsFooArray): IsFooArray; Name: InputTypeKind.Dictionary, IsNullable: false, KeyType: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, }, ValueType: { @@ -695,3 +694,36 @@ op op5(@body body: IsFooArray): IsFooArray; ); }); }); + +describe("Empty models should be returned by tsp", () => { + let runner: TestHost; + + beforeEach(async () => { + runner = await createEmitterTestHost(); + }); + + it("Empty Model should be returned", async () => { + const program = await typeSpecCompile( + ` +@doc("Empty model") +@usage(Usage.input) +@access(Access.public) +model Empty { +} + +@route("/op1") +op op1(): void; +`, + runner, + { IsTCGCNeeded: true } + ); + runner.compileAndDiagnose; + const context = createEmitterContext(program); + const sdkContext = createNetSdkContext(context); + const root = createModel(sdkContext); + const models = root.Models; + const isEmptyModel = models.find((m) => m.Name === "Empty"); + assert(isEmptyModel !== undefined); + // assert the inherited dictionary type is expected + }); +}); diff --git a/packages/http-client-csharp/emitter/test/Unit/property-type.test.ts b/packages/http-client-csharp/emitter/test/Unit/property-type.test.ts index e35bfb2a0d4..104c6131c39 100644 --- a/packages/http-client-csharp/emitter/test/Unit/property-type.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/property-type.test.ts @@ -2,7 +2,6 @@ import { TestHost } from "@typespec/compiler/testing"; import assert, { deepStrictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { createModel } from "../../src/lib/client-model-builder.js"; -import { InputPrimitiveTypeKind } from "../../src/type/input-primitive-type-kind.js"; import { InputTypeKind } from "../../src/type/input-type-kind.js"; import { InputEnumType, InputListType } from "../../src/type/input-type.js"; import { @@ -36,9 +35,9 @@ describe("Test GetInputType for array", () => { Kind: InputTypeKind.Array, Name: InputTypeKind.Array, ElementType: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, + Encode: undefined, }, IsNullable: false, } as InputListType, @@ -62,9 +61,9 @@ describe("Test GetInputType for array", () => { Kind: InputTypeKind.Array, Name: InputTypeKind.Array, ElementType: { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.String, + Kind: "string", IsNullable: false, + Encode: undefined, }, IsNullable: false, } as InputListType, @@ -106,14 +105,18 @@ describe("Test GetInputType for enum", () => { const root = createModel(sdkContext); deepStrictEqual( { - Kind: InputTypeKind.Enum, + Kind: "enum", Name: "SimpleEnum", Namespace: "Azure.Csharp.Testing", Accessibility: undefined, Deprecated: undefined, Description: "fixed string enum", - EnumValueType: "String", - AllowedValues: [ + ValueType: { + Kind: "string", + IsNullable: false, + Encode: undefined, + }, + Values: [ { Name: "One", Value: "1", @@ -140,7 +143,7 @@ describe("Test GetInputType for enum", () => { )}` ); const type = root.Clients[0].Operations[0].Parameters[0].Type as InputEnumType; - assert(type.EnumValueType !== undefined); + assert(type.ValueType !== undefined); deepStrictEqual(type.Name, "SimpleEnum"); deepStrictEqual(type.IsExtensible, false); }); @@ -170,14 +173,18 @@ describe("Test GetInputType for enum", () => { const root = createModel(sdkContext); deepStrictEqual( { - Kind: InputTypeKind.Enum, + Kind: "enum", Name: "FixedIntEnum", Namespace: "Azure.Csharp.Testing", Accessibility: undefined, Deprecated: undefined, Description: "Fixed int enum", - EnumValueType: "Int32", - AllowedValues: [ + ValueType: { + Kind: "int32", + IsNullable: false, + Encode: undefined, + }, + Values: [ { Name: "One", Value: 1, @@ -204,7 +211,7 @@ describe("Test GetInputType for enum", () => { )}` ); const type = root.Clients[0].Operations[0].Parameters[0].Type as InputEnumType; - assert(type.EnumValueType !== undefined); + assert(type.ValueType !== undefined); deepStrictEqual(type.Name, "FixedIntEnum"); deepStrictEqual(type.IsExtensible, false); }); @@ -227,14 +234,18 @@ describe("Test GetInputType for enum", () => { const root = createModel(sdkContext); deepStrictEqual( { - Kind: InputTypeKind.Enum, + Kind: "enum", Name: "FixedEnum", Namespace: "Azure.Csharp.Testing", Accessibility: undefined, Deprecated: undefined, Description: "Fixed enum", - EnumValueType: "String", - AllowedValues: [ + ValueType: { + Kind: "string", + IsNullable: false, + Encode: undefined, + }, + Values: [ { Name: "One", Value: "1", Description: undefined }, { Name: "Two", Value: "2", Description: undefined }, { Name: "Four", Value: "4", Description: undefined }, @@ -249,7 +260,7 @@ describe("Test GetInputType for enum", () => { )}` ); const type = root.Clients[0].Operations[0].Parameters[0].Type as InputEnumType; - assert(type.EnumValueType !== undefined); + assert(type.ValueType !== undefined); deepStrictEqual(type.Name, "FixedEnum"); deepStrictEqual((type as InputEnumType).IsExtensible, false); }); diff --git a/packages/http-client-csharp/emitter/test/Unit/scalar.test.ts b/packages/http-client-csharp/emitter/test/Unit/scalar.test.ts index d7fd06f8d93..0e06d186081 100644 --- a/packages/http-client-csharp/emitter/test/Unit/scalar.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/scalar.test.ts @@ -2,8 +2,6 @@ import { TestHost } from "@typespec/compiler/testing"; import { deepStrictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { createModel } from "../../src/lib/client-model-builder.js"; -import { InputPrimitiveTypeKind } from "../../src/type/input-primitive-type-kind.js"; -import { InputTypeKind } from "../../src/type/input-type-kind.js"; import { createEmitterContext, createEmitterTestHost, @@ -30,16 +28,12 @@ describe("Test GetInputType for scalar", () => { const context = createEmitterContext(program); const sdkContext = createNetSdkContext(context); const root = createModel(sdkContext); - deepStrictEqual(root.Clients[0].Operations[0].Parameters[0].Type.Kind, InputTypeKind.Primitive); - deepStrictEqual( - root.Clients[0].Operations[0].Parameters[0].Type.Name, - InputPrimitiveTypeKind.AzureLocation - ); + deepStrictEqual(root.Clients[0].Operations[0].Parameters[0].Type.Kind, "azureLocation"); deepStrictEqual( { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.AzureLocation, + Kind: "azureLocation", IsNullable: false, + Encode: "string", }, root.Clients[0].Operations[0].Parameters[0].Type ); diff --git a/packages/http-client-csharp/emitter/test/Unit/string-format.test.ts b/packages/http-client-csharp/emitter/test/Unit/string-format.test.ts index 187a834f02c..14d56deedab 100644 --- a/packages/http-client-csharp/emitter/test/Unit/string-format.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/string-format.test.ts @@ -3,8 +3,6 @@ import { getAllHttpServices } from "@typespec/http"; import assert, { deepStrictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { loadOperation } from "../../src/lib/operation.js"; -import { InputPrimitiveTypeKind } from "../../src/type/input-primitive-type-kind.js"; -import { InputTypeKind } from "../../src/type/input-type-kind.js"; import { InputEnumType, InputModelType, InputPrimitiveType } from "../../src/type/input-type.js"; import { createEmitterContext, @@ -44,9 +42,9 @@ describe("Test string format", () => { ); deepStrictEqual( { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.Uri, + Kind: "url", IsNullable: false, + Encode: undefined, } as InputPrimitiveType, operation.Parameters[0].Type ); @@ -73,132 +71,9 @@ describe("Test string format", () => { assert(foo !== undefined); deepStrictEqual( { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.Uri, - IsNullable: false, - } as InputPrimitiveType, - foo.Properties[0].Type - ); - }); - - it("format uri on operation parameter", async () => { - const program = await typeSpecCompile( - ` - op test(@path @format("uri")sourceUrl: string): void; - `, - runner - ); - const context = createEmitterContext(program); - const sdkContext = createNetSdkContext(context); - const [services] = getAllHttpServices(program); - const modelMap = new Map(); - const enumMap = new Map(); - const operation = loadOperation( - sdkContext, - services[0].operations[0], - "", - [], - services[0].namespace, - modelMap, - enumMap - ); - deepStrictEqual( - { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.Uri, - IsNullable: false, - }, - operation.Parameters[0].Type - ); - }); - - it("format uri on model property", async () => { - const program = await typeSpecCompile( - ` - @doc("This is a model.") - model Foo { - @doc("The source url.") - @format("uri") - source: string - } - `, - runner - ); - const context = createEmitterContext(program); - const sdkContext = createNetSdkContext(context); - const [services] = getAllHttpServices(program); - const modelMap = new Map(); - const enumMap = new Map(); - navigateModels(sdkContext, services[0].namespace, modelMap, enumMap); - const foo = modelMap.get("Foo"); - assert(foo !== undefined); - deepStrictEqual( - { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.Uri, - IsNullable: false, - }, - foo.Properties[0].Type, - `string property format is not correct. Got ${JSON.stringify(foo.Properties[0].Type)}` - ); - }); - - it("format uuid on operation parameter", async () => { - const program = await typeSpecCompile( - ` - op test(@path @format("uuid")subscriptionId: string): void; - `, - runner - ); - const context = createEmitterContext(program); - const sdkContext = createNetSdkContext(context); - const [services] = getAllHttpServices(program); - const modelMap = new Map(); - const enumMap = new Map(); - const operation = loadOperation( - sdkContext, - services[0].operations[0], - "", - [], - services[0].namespace, - modelMap, - enumMap - ); - deepStrictEqual( - { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.Guid, - IsNullable: false, - }, - operation.Parameters[0].Type - ); - }); - - it("format on model property", async () => { - const program = await typeSpecCompile( - ` - @doc("This is a model.") - model Foo { - @doc("The subscription id.") - @format("uuid") - subscriptionId: string - } - `, - runner - ); - const context = createEmitterContext(program); - const sdkContext = createNetSdkContext(context); - const [services] = getAllHttpServices(program); - const modelMap = new Map(); - const enumMap = new Map(); - navigateModels(sdkContext, services[0].namespace, modelMap, enumMap); - const foo = modelMap.get("Foo"); - assert(foo !== undefined); - deepStrictEqual( - { - Kind: InputTypeKind.Primitive, - Name: InputPrimitiveTypeKind.Guid, + Kind: "url", IsNullable: false, + Encode: undefined, } as InputPrimitiveType, foo.Properties[0].Type ); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/ClientModelPlugin.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/ClientModelPlugin.cs index 959e89ac8f7..a16f0f073dd 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/ClientModelPlugin.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/ClientModelPlugin.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; using System.ComponentModel.Composition; -using Microsoft.Generator.CSharp.ClientModel.Expressions; -using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.ClientModel.Snippets; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Snippets; namespace Microsoft.Generator.CSharp.ClientModel { @@ -14,12 +16,11 @@ public class ClientModelPlugin : CodeModelPlugin private static ClientModelPlugin? _instance; internal static ClientModelPlugin Instance => _instance ?? throw new InvalidOperationException("ClientModelPlugin is not loaded."); public override ApiTypes ApiTypes { get; } - public override CodeWriterExtensionMethods CodeWriterExtensionMethods { get; } private OutputLibrary? _scmOutputLibrary; public override OutputLibrary OutputLibrary => _scmOutputLibrary ??= new(); - public override TypeProviderWriter GetWriter(CodeWriter writer, TypeProvider provider) => new(writer, provider); + public override TypeProviderWriter GetWriter(TypeProvider provider) => new(provider); public override TypeFactory TypeFactory { get; } @@ -29,10 +30,11 @@ public class ClientModelPlugin : CodeModelPlugin /// Returns the serialization type providers for the given model type provider. /// /// The model type provider. - public override IReadOnlyList GetSerializationTypeProviders(ModelTypeProvider provider) + /// The input model. + public override IReadOnlyList GetSerializationTypeProviders(ModelProvider provider, InputModelType inputModel) { - // Add JSON serialization type provider - return [new MrwSerializationTypeProvider(provider)]; + // Add MRW serialization type provider + return [new MrwSerializationTypeProvider(provider, inputModel)]; } [ImportingConstructor] @@ -42,7 +44,6 @@ public ClientModelPlugin(GeneratorContext context) TypeFactory = new ScmTypeFactory(); ExtensibleSnippets = new SystemExtensibleSnippets(); ApiTypes = new SystemApiTypes(); - CodeWriterExtensionMethods = new(); _instance = this; } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/ClientResultExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/ClientResultExpression.cs deleted file mode 100644 index 10977817b34..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/ClientResultExpression.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.ClientModel; -using Microsoft.Generator.CSharp.Expressions; - -namespace Microsoft.Generator.CSharp.ClientModel.Expressions -{ - internal sealed record ClientResultExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) - { - public ValueExpression Value => Property(nameof(ClientResult.Value)); - public BinaryDataExpression Content => throw new InvalidOperationException("Result does not have a Content property"); - public StreamExpression ContentStream => throw new InvalidOperationException("Result does not have a ContentStream property"); - - public static ClientResultExpression FromResponse(PipelineResponseExpression response) - => new(InvokeStatic(nameof(ClientResult.FromResponse), response)); - - public static ClientResultExpression FromValue(ValueExpression value, PipelineResponseExpression response) - => new(InvokeStatic(nameof(ClientResult.FromValue), value, response)); - - public static ClientResultExpression FromValue(CSharpType explicitValueType, ValueExpression value, PipelineResponseExpression response) - => new(new InvokeStaticMethodExpression(typeof(ClientResult), nameof(ClientResult.FromValue), new[] { value, response }, new[] { explicitValueType })); - - public ClientResultExpression FromValue(ValueExpression value) - => new(new InvokeStaticMethodExpression(typeof(ClientResult), nameof(ClientResult.FromValue), new[] { value, this })); - - public ClientResultExpression FromValue(CSharpType explicitValueType, ValueExpression value) - => new(new InvokeStaticMethodExpression(typeof(ClientResult), nameof(ClientResult.FromValue), new[] { value, this }, new[] { explicitValueType })); - - public PipelineResponseExpression GetRawResponse() => new(Invoke(nameof(ClientResult.GetRawResponse))); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/PipelineMessageExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/PipelineMessageExpression.cs deleted file mode 100644 index 2f5753cd000..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/PipelineMessageExpression.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.ClientModel.Primitives; -using Microsoft.Generator.CSharp.Expressions; - -namespace Microsoft.Generator.CSharp.ClientModel.Expressions -{ - internal sealed record PipelineMessageExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) - { - public PipelineRequestExpression Request => new(Property(nameof(PipelineMessage.Request))); - - public PipelineResponseExpression Response => new(Property(nameof(PipelineMessage.Response))); - - public BoolExpression BufferResponse => new(Property(nameof(PipelineMessage.BufferResponse))); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/PipelineResponseExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/PipelineResponseExpression.cs deleted file mode 100644 index e0e2f65a974..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/PipelineResponseExpression.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.ClientModel.Primitives; -using Microsoft.Generator.CSharp.Expressions; - -namespace Microsoft.Generator.CSharp.ClientModel.Expressions -{ - internal sealed record PipelineResponseExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) - { - public BinaryDataExpression Content => new(Property(nameof(PipelineResponse.Content))); - - public StreamExpression ContentStream => new(Property(nameof(PipelineResponse.ContentStream))); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/SystemExtensibleSnippets.SystemModelSnippets.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/SystemExtensibleSnippets.SystemModelSnippets.cs deleted file mode 100644 index 210afb97cc6..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/SystemExtensibleSnippets.SystemModelSnippets.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.ClientModel.Primitives; -using Microsoft.Generator.CSharp.Expressions; - -namespace Microsoft.Generator.CSharp.ClientModel.Expressions -{ - internal partial class SystemExtensibleSnippets - { - internal class SystemModelSnippets : ModelSnippets - { - public override CSharpMethod BuildFromOperationResponseMethod(TypeProvider typeProvider, MethodSignatureModifiers modifiers) - { - var result = new Parameter("response", $"The result to deserialize the model from.", typeof(PipelineResponse), null, ValidationType.None, null); - return new CSharpMethod - ( - new MethodSignature(ClientModelPlugin.Instance.Configuration.ApiTypes.FromResponseName, null, $"Deserializes the model from a raw response.", modifiers, typeProvider.Type, null, new[] { result }), - new MethodBodyStatement[] - { - Snippets.UsingVar("document", JsonDocumentExpression.Parse(new PipelineResponseExpression(result).Content), out var document), - Snippets.Return(ObjectTypeExpression.Deserialize(typeProvider, document.RootElement)) - }, - "default" - ); - } - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/SystemExtensibleSnippets.SystemRestOperationsSnippets.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/SystemExtensibleSnippets.SystemRestOperationsSnippets.cs deleted file mode 100644 index 5b4db9fc141..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/SystemExtensibleSnippets.SystemRestOperationsSnippets.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.ClientModel; -using System.ClientModel.Internal; -using System.ClientModel.Primitives; -using System.Linq; -using Microsoft.Generator.CSharp.Expressions; -using Microsoft.Generator.CSharp.Input; - -namespace Microsoft.Generator.CSharp.ClientModel.Expressions -{ - internal partial class SystemExtensibleSnippets - { - private class SystemRestOperationsSnippets : RestOperationsSnippets - { - public override StreamExpression GetContentStream(TypedValueExpression result) - => new ClientResultExpression(result).GetRawResponse().ContentStream; - - public override TypedValueExpression GetTypedResponseFromValue(TypedValueExpression value, TypedValueExpression result) - { - return ClientResultExpression.FromValue(value, GetRawResponse(result)); - } - - public override TypedValueExpression GetTypedResponseFromModel(TypeProvider typeProvider, TypedValueExpression result) - { - var response = GetRawResponse(result); - var model = new InvokeStaticMethodExpression(typeProvider.Type, ClientModelPlugin.Instance.Configuration.ApiTypes.FromResponseName, new[] { response }); - return ClientResultExpression.FromValue(model, response); - } - - public override TypedValueExpression GetTypedResponseFromEnum(EnumTypeProvider enumType, TypedValueExpression result) - { - var response = GetRawResponse(result); - return ClientResultExpression.FromValue(EnumExpression.ToEnum(enumType, response.Content.ToObjectFromJson(typeof(string))), response); - } - - public override TypedValueExpression GetTypedResponseFromBinaryData(Type responseType, TypedValueExpression result, string? contentType = null) - { - var rawResponse = GetRawResponse(result); - if (responseType == typeof(string) && contentType != null && FormattableStringHelpers.ToMediaType(contentType) == BodyMediaType.Text) - { - return ClientResultExpression.FromValue(rawResponse.Content.InvokeToString(), rawResponse); - } - return responseType == typeof(BinaryData) - ? ClientResultExpression.FromValue(rawResponse.Content, rawResponse) - : ClientResultExpression.FromValue(rawResponse.Content.ToObjectFromJson(responseType), rawResponse); - } - - public override MethodBodyStatement DeclareHttpMessage(MethodSignatureBase createRequestMethodSignature, out TypedValueExpression message) - { - var messageVar = new VariableReference(typeof(PipelineMessage), "message"); - message = messageVar; - return Snippets.UsingDeclare(messageVar, new InvokeInstanceMethodExpression(null, createRequestMethodSignature.Name, createRequestMethodSignature.Parameters.Select(p => (ValueExpression)p).ToList(), null, false)); - } - - public override MethodBodyStatement DeclareContentWithUtf8JsonWriter(out TypedValueExpression content, out Utf8JsonWriterExpression writer) - { - var contentVar = new VariableReference(typeof(BinaryContent), "content"); - content = contentVar; - writer = new Utf8JsonWriterExpression(content.Property("JsonWriter")); - return Snippets.Var(contentVar, Snippets.New.Instance(typeof(BinaryContent))); - } - - private static PipelineResponseExpression GetRawResponse(TypedValueExpression result) - => result.Type.Equals(typeof(PipelineResponse)) - ? new PipelineResponseExpression(result) - : new ClientResultExpression(result).GetRawResponse(); - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/SystemSnippets.Serializations.JsonFormat.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/SystemSnippets.Serializations.JsonFormat.cs deleted file mode 100644 index eb23798001a..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/SystemSnippets.Serializations.JsonFormat.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.Generator.CSharp.Expressions; - -namespace Microsoft.Generator.CSharp.ClientModel.Expressions -{ - internal static partial class SystemSnippets - { - internal static StringExpression JsonFormatSerialization = Snippets.Literal("J"); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Microsoft.Generator.CSharp.ClientModel.csproj b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Microsoft.Generator.CSharp.ClientModel.csproj index 90c382eae19..fa4d734d767 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Microsoft.Generator.CSharp.ClientModel.csproj +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Microsoft.Generator.CSharp.ClientModel.csproj @@ -1,4 +1,4 @@ - + Microsoft.Generator.CSharp.ClientModel @@ -17,6 +17,10 @@ + + + + diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Models/Types/ClientPipelineExtensionsProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Models/Types/ClientPipelineExtensionsProvider.cs index 4e67a2132c7..5e8382729a3 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Models/Types/ClientPipelineExtensionsProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Models/Types/ClientPipelineExtensionsProvider.cs @@ -2,11 +2,12 @@ // Licensed under the MIT License. using System; -using Microsoft.Generator.CSharp.Expressions; using System.ClientModel.Primitives; -using System.ClientModel; -using Microsoft.Generator.CSharp.ClientModel.Expressions; using System.Collections.Generic; +using Microsoft.Generator.CSharp.ClientModel.Snippets; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Snippets; namespace Microsoft.Generator.CSharp.ClientModel { @@ -21,24 +22,23 @@ internal class ClientPipelineExtensionsProvider : TypeProvider private const string _processHeadAsBoolMessageAsync = "ProcessHeadAsBoolMessageAsync"; private const string _processHeadAsBoolMessage = "ProcessHeadAsBoolMessage"; - private Parameter _pipelineParam; - private Parameter _messageParam; - private Parameter _requestContextParam; - private ParameterReference _pipeline; - private ParameterReference _message; - private ParameterReference _requestContext; + private ParameterProvider _pipelineParam; + private ParameterProvider _messageParam; + private ParameterProvider _requestContextParam; + private ParameterReferenceSnippet _pipeline; + private ParameterReferenceSnippet _message; + private ParameterReferenceSnippet _requestContext; private MemberExpression _messageResponse; private ClientPipelineExtensionsProvider() - : base(null) { Name = "ClientPipelineExtensions"; - _pipelineParam = new Parameter("pipeline", null, typeof(ClientPipeline), null, ValidationType.None, null); - _messageParam = new Parameter("message", null, typeof(PipelineMessage), null, ValidationType.None, null); - _requestContextParam = new Parameter("requestContext", null, typeof(RequestOptions), null, ValidationType.None, null); - _pipeline = new ParameterReference(_pipelineParam); - _message = new ParameterReference(_messageParam); - _requestContext = new ParameterReference(_requestContextParam); + _pipelineParam = new ParameterProvider("pipeline", $"The pipeline.", typeof(ClientPipeline)); + _messageParam = new ParameterProvider("message", $"The message.", typeof(PipelineMessage)); + _requestContextParam = new ParameterProvider("requestContext", $"The request context.", typeof(RequestOptions)); + _pipeline = new ParameterReferenceSnippet(_pipelineParam); + _message = new ParameterReferenceSnippet(_messageParam); + _requestContext = new ParameterReferenceSnippet(_requestContextParam); _messageResponse = new MemberExpression(_message, "Response"); } @@ -47,12 +47,12 @@ protected override TypeSignatureModifiers GetDeclarationModifiers() return TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static; } - internal PipelineResponseExpression ProcessMessage(IReadOnlyList arguments, bool isAsync) + internal PipelineResponseSnippet ProcessMessage(IReadOnlyList arguments, bool isAsync) { return new(new InvokeStaticMethodExpression(Type, isAsync ? _processMessageAsync : _processMessage, arguments, CallAsExtension: true, CallAsAsync: isAsync)); } - internal ClientResultExpression ProcessHeadAsBoolMessage(IReadOnlyList arguments, bool isAsync) + internal ClientResultSnippet ProcessHeadAsBoolMessage(IReadOnlyList arguments, bool isAsync) { return new(new InvokeStaticMethodExpression(Type, isAsync ? _processHeadAsBoolMessageAsync : _processHeadAsBoolMessage, arguments, CallAsExtension: true, CallAsAsync: isAsync)); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Models/Types/MrwSerializationTypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Models/Types/MrwSerializationTypeProvider.cs index 08f7f3fd16a..a6482de70e8 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Models/Types/MrwSerializationTypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Models/Types/MrwSerializationTypeProvider.cs @@ -3,10 +3,17 @@ using System; using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text.Json; -using Microsoft.Generator.CSharp.ClientModel.Expressions; +using Microsoft.Generator.CSharp.ClientModel.Snippets; using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Snippets; +using Microsoft.Generator.CSharp.Statements; +using static Microsoft.Generator.CSharp.Snippets.Snippet; namespace Microsoft.Generator.CSharp.ClientModel { @@ -15,42 +22,123 @@ namespace Microsoft.Generator.CSharp.ClientModel /// internal sealed class MrwSerializationTypeProvider : TypeProvider { - private readonly Parameter SerializationOptionsParameter = - new("options", null, typeof(ModelReaderWriterOptions), null, ValidationType.None, null); + private readonly ParameterProvider _serializationOptionsParameter = + new("options", $"The client options for reading and writing models.", typeof(ModelReaderWriterOptions)); + private const string _privateAdditionalPropertiesPropertyDescription = "Keeps track of any properties unknown to the library."; + private const string _privateAdditionalPropertiesPropertyName = "_serializedAdditionalRawData"; + private static readonly CSharpType _privateAdditionalPropertiesPropertyType = typeof(IDictionary); private readonly CSharpType _iJsonModelTInterface; private readonly CSharpType? _iJsonModelObjectInterface; private readonly CSharpType _iPersistableModelTInterface; private readonly CSharpType? _iPersistableModelObjectInterface; - private readonly ModelTypeProvider _model; + private ModelProvider _model; + private InputModelType _inputModel; + private readonly FieldProvider? _rawDataField; private readonly bool _isStruct; - public MrwSerializationTypeProvider(ModelTypeProvider model) : base(null) + public MrwSerializationTypeProvider(ModelProvider model, InputModelType inputModel) { _model = model; + _inputModel = inputModel; _isStruct = model.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Struct); - Name = model.Name; - Namespace = model.Namespace; // Initialize the serialization interfaces - _iJsonModelTInterface = new CSharpType(typeof(IJsonModel<>), _model.Type); + _iJsonModelTInterface = new CSharpType(typeof(IJsonModel<>), model.Type); _iJsonModelObjectInterface = _isStruct ? (CSharpType)typeof(IJsonModel) : null; - _iPersistableModelTInterface = new CSharpType(typeof(IPersistableModel<>), _model.Type); + _iPersistableModelTInterface = new CSharpType(typeof(IPersistableModel<>), model.Type); _iPersistableModelObjectInterface = _isStruct ? (CSharpType)typeof(IPersistableModel) : null; + _rawDataField = BuildRawDataField(); + + Name = model.Name; + Namespace = model.Namespace; } + protected override string GetFileName() => Path.Combine("src", "Generated", "Models", $"{Name}.serialization.cs"); + protected override TypeSignatureModifiers GetDeclarationModifiers() => _model.DeclarationModifiers; public override string Name { get; } public override string Namespace { get; } /// - /// Builds the serialization methods for the model. If the serialization supports JSON, it will build the JSON serialization methods. + /// Builds the fields for the model by adding the raw data field for serialization. + /// + /// The list of for the model. + protected override FieldProvider[] BuildFields() + { + return _rawDataField != null ? [_rawDataField] : Array.Empty(); + } + + protected override MethodProvider[] BuildConstructors() + { + List constructors = new List(); + bool serializationCtorParamsMatch = false; + bool ctorWithNoParamsExist = false; + MethodProvider serializationConstructor = BuildSerializationConstructor(); + + foreach (var ctor in _model.Constructors) + { + var initializationCtorParams = ctor.Signature.Parameters; + + // Check if the model constructor has no parameters + if (!ctorWithNoParamsExist && !initializationCtorParams.Any()) + { + ctorWithNoParamsExist = true; + } + + + if (!serializationCtorParamsMatch) + { + // Check if the model constructor parameters match the serialization constructor parameters + if (initializationCtorParams.SequenceEqual(serializationConstructor.Signature.Parameters)) + { + serializationCtorParamsMatch = true; + } + } + } + + // Add the serialization constructor if it doesn't match any of the existing constructors + if (!serializationCtorParamsMatch) + { + constructors.Add(serializationConstructor); + } + + // Add an empty constructor if the model doesn't have one + if (!ctorWithNoParamsExist) + { + constructors.Add(BuildEmptyConstructor()); + } + + return constructors.ToArray(); + } + + /// + /// Builds the raw data field for the model to be used for serialization. + /// + /// The constructed if the model should generate the field. + private FieldProvider? BuildRawDataField() + { + if (_isStruct) + { + return null; + } + + var FieldProvider = new FieldProvider( + modifiers: FieldModifiers.Private, + type: _privateAdditionalPropertiesPropertyType, + name: _privateAdditionalPropertiesPropertyName); + + return FieldProvider; + } + + /// + /// Builds the serialization methods for the model. /// /// A list of serialization and deserialization methods for the model. - protected override CSharpMethod[] BuildMethods() + protected override MethodProvider[] BuildMethods() { // TO-DO: Add deserialization methods https://github.com/microsoft/typespec/issues/3330 - return new CSharpMethod[] + return new MethodProvider[] { // Add JSON serialization methods BuildJsonModelWriteMethod(), @@ -83,79 +171,167 @@ protected override CSharpType[] BuildImplements() /// /// Builds the JSON serialization write method for the model. /// - internal CSharpMethod BuildJsonModelWriteMethod() + internal MethodProvider BuildJsonModelWriteMethod() { - Parameter utf8JsonWriterParameter = new("writer", null, typeof(Utf8JsonWriter), null, ValidationType.None, null); + ParameterProvider utf8JsonWriterParameter = new("writer", $"The JSON writer.", typeof(Utf8JsonWriter)); // void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) - return new CSharpMethod + return new MethodProvider ( - new MethodSignature(nameof(IJsonModel.Write), null, null, MethodSignatureModifiers.None, null, null, new[] { utf8JsonWriterParameter, SerializationOptionsParameter }, ExplicitInterface: _iJsonModelTInterface), + new MethodSignature(nameof(IJsonModel.Write), null, null, MethodSignatureModifiers.None, null, null, new[] { utf8JsonWriterParameter, _serializationOptionsParameter }, ExplicitInterface: _iJsonModelTInterface), // TO-DO: Add body for json properties' serialization https://github.com/microsoft/typespec/issues/3330 - Snippets.EmptyStatement + Snippet.EmptyStatement ); } /// /// Builds the JSON serialization create method for the model. /// - internal CSharpMethod BuildJsonModelCreateMethod() + internal MethodProvider BuildJsonModelCreateMethod() { - Parameter utf8JsonReaderParameter = new("reader", null, typeof(Utf8JsonReader), null, ValidationType.None, null, IsRef: true); + ParameterProvider utf8JsonReaderParameter = new("reader", $"The JSON reader.", typeof(Utf8JsonReader), isRef: true); // T IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) var typeOfT = GetModelArgumentType(_iJsonModelTInterface); - return new CSharpMethod + return new MethodProvider ( - new MethodSignature(nameof(IJsonModel.Create), null, null, MethodSignatureModifiers.None, typeOfT, null, new[] { utf8JsonReaderParameter, SerializationOptionsParameter }, ExplicitInterface: _iJsonModelTInterface), + new MethodSignature(nameof(IJsonModel.Create), null, null, MethodSignatureModifiers.None, typeOfT, null, new[] { utf8JsonReaderParameter, _serializationOptionsParameter }, ExplicitInterface: _iJsonModelTInterface), // TO-DO: Call the base model ctor for now until the model properties are serialized https://github.com/microsoft/typespec/issues/3330 - Snippets.Return(new NewInstanceExpression(typeOfT, Array.Empty())) + Snippet.Return(new NewInstanceExpression(typeOfT, Array.Empty())) ); } /// /// Builds the I model write method. /// - internal CSharpMethod BuildIModelWriteMethod() + internal MethodProvider BuildIModelWriteMethod() { // BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) var returnType = typeof(BinaryData); - return new CSharpMethod + return new MethodProvider ( - new MethodSignature(nameof(IPersistableModel.Write), null, null, MethodSignatureModifiers.None, returnType, null, new[] { SerializationOptionsParameter }, ExplicitInterface: _iPersistableModelTInterface), + new MethodSignature(nameof(IPersistableModel.Write), null, null, MethodSignatureModifiers.None, returnType, null, new[] { _serializationOptionsParameter }, ExplicitInterface: _iPersistableModelTInterface), // TO-DO: Call the base model ctor for now until the model properties are serialized https://github.com/microsoft/typespec/issues/3330 - Snippets.Return(new NewInstanceExpression(returnType, new ValueExpression[] { new StringLiteralExpression(_iPersistableModelTInterface.Name, false) })) + Snippet.Return(new NewInstanceExpression(returnType, [Snippet.Literal(_iPersistableModelTInterface.Name)])) ); } /// /// Builds the I model create method. /// - internal CSharpMethod BuildIModelCreateMethod() + internal MethodProvider BuildIModelCreateMethod() { - Parameter dataParameter = new("data", null, typeof(BinaryData), null, ValidationType.None, null); + ParameterProvider dataParameter = new("data", $"The data to parse.", typeof(BinaryData)); // IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) var typeOfT = GetModelArgumentType(_iPersistableModelTInterface); - return new CSharpMethod + return new MethodProvider ( - new MethodSignature(nameof(IPersistableModel.Create), null, null, MethodSignatureModifiers.None, typeOfT, null, new[] { dataParameter, SerializationOptionsParameter }, ExplicitInterface: _iPersistableModelTInterface), + new MethodSignature(nameof(IPersistableModel.Create), null, null, MethodSignatureModifiers.None, typeOfT, null, new[] { dataParameter, _serializationOptionsParameter }, ExplicitInterface: _iPersistableModelTInterface), // TO-DO: Call the base model ctor for now until the model properties are serialized https://github.com/microsoft/typespec/issues/3330 - Snippets.Return(new NewInstanceExpression(typeOfT, Array.Empty())) + Snippet.Return(new NewInstanceExpression(typeOfT, Array.Empty())) ); } /// /// Builds the I model GetFormatFromOptions method. /// - internal CSharpMethod BuildIModelGetFormatFromOptionsMethod() + internal MethodProvider BuildIModelGetFormatFromOptionsMethod() { ValueExpression jsonWireFormat = SystemSnippets.JsonFormatSerialization; // ModelReaderWriterFormat IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) - return new CSharpMethod + return new MethodProvider ( - new MethodSignature(nameof(IPersistableModel.GetFormatFromOptions), null, null, MethodSignatureModifiers.None, typeof(string), null, new[] { SerializationOptionsParameter }, ExplicitInterface: _iPersistableModelTInterface), + new MethodSignature(nameof(IPersistableModel.GetFormatFromOptions), null, null, MethodSignatureModifiers.None, typeof(string), null, new[] { _serializationOptionsParameter }, ExplicitInterface: _iPersistableModelTInterface), jsonWireFormat ); } + /// + /// Builds the serialization constructor for the model. + /// + /// The constructed serialization constructor. + internal MethodProvider BuildSerializationConstructor() + { + var serializationCtorParameters = BuildSerializationConstructorParameters(); + + return new MethodProvider( + signature: new ConstructorSignature( + Type, + $"Initializes a new instance of {Type:C}", + null, + MethodSignatureModifiers.Internal, + serializationCtorParameters), + bodyStatements: new MethodBodyStatement[] + { + GetPropertyInitializers(serializationCtorParameters) + }); + } + + private MethodBodyStatement GetPropertyInitializers(IReadOnlyList parameters) + { + List methodBodyStatements = new(); + + foreach (var param in parameters) + { + if (param.Name == _rawDataField?.Name.ToVariableName()) + { + methodBodyStatements.Add(Assign(new MemberExpression(null, _rawDataField.Name), new ParameterReferenceSnippet(param))); + continue; + } + + ValueExpression initializationValue = new ParameterReferenceSnippet(param); + var initializationStatement = Assign(new MemberExpression(null, param.Name.FirstCharToUpperCase()), initializationValue); + if (initializationStatement != null) + { + methodBodyStatements.Add(initializationStatement); + } + } + + return methodBodyStatements; + } + + /// + /// Builds the parameters for the serialization constructor by iterating through the input model properties. + /// It then adds raw data field to the constructor if it doesn't already exist in the list of constructed parameters. + /// + /// The list of parameters for the serialization parameter. + private List BuildSerializationConstructorParameters() + { + List constructorParameters = new List(); + bool shouldAddRawDataField = _rawDataField != null; + + foreach (var property in _inputModel.Properties) + { + var parameter = new ParameterProvider(property) + { + Validation = ParameterValidationType.None, + }; + constructorParameters.Add(parameter); + + if (shouldAddRawDataField && string.Equals(parameter.Name, _rawDataField?.Name, StringComparison.OrdinalIgnoreCase)) + { + shouldAddRawDataField = false; + } + } + + // Append the raw data field if it doesn't already exist in the constructor parameters + if (shouldAddRawDataField && _rawDataField != null) + { + constructorParameters.Add(new ParameterProvider( + _rawDataField.Name.ToVariableName(), + FormattableStringHelpers.FromString(_privateAdditionalPropertiesPropertyDescription), + _rawDataField.Type)); + } + + return constructorParameters; + } + + private MethodProvider BuildEmptyConstructor() + { + var accessibility = _isStruct ? MethodSignatureModifiers.Public : MethodSignatureModifiers.Internal; + return new MethodProvider( + signature: new ConstructorSignature(Type, $"Initializes a new instance of {Type:C} for deserialization.", null, accessibility, Array.Empty()), + bodyStatements: new MethodBodyStatement()); + } + /// /// Attempts to get the model argument type from the model interface. /// diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/ScmTypeFactory.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/ScmTypeFactory.cs index b0cd3637b2e..b2537161bbb 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/ScmTypeFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/ScmTypeFactory.cs @@ -7,8 +7,7 @@ using System.Linq; using System.Net; using Microsoft.Generator.CSharp.Input; -using System.Net.Http; -using System.Net.Http.Headers; +using Microsoft.Generator.CSharp.Providers; namespace Microsoft.Generator.CSharp.ClientModel { @@ -21,66 +20,54 @@ internal class ScmTypeFactory : TypeFactory /// The of the input type. public override CSharpType CreateCSharpType(InputType inputType) => inputType switch { - InputLiteralType literalType => CSharpType.FromLiteral(CreateCSharpType(literalType.LiteralValueType), literalType.Value), - InputUnionType unionType => CSharpType.FromUnion(unionType.UnionItemTypes.Select(CreateCSharpType).ToArray(), unionType.IsNullable), - InputList { IsEmbeddingsVector: true } listType => new CSharpType(typeof(ReadOnlyMemory<>), listType.IsNullable, CreateCSharpType(listType.ElementType)), - InputList listType => new CSharpType(typeof(IList<>), listType.IsNullable, CreateCSharpType(listType.ElementType)), - InputDictionary dictionaryType => new CSharpType(typeof(IDictionary<,>), inputType.IsNullable, typeof(string), CreateCSharpType(dictionaryType.ValueType)), - // Uncomment this when the enums are implemented: https://github.com/Azure/autorest.csharp/issues/4579 - //InputEnumType enumType => ClientModelPlugin.Instance.OutputLibrary.EnumMappings.TryGetValue(enumType, out var provider) - //? provider.Type.WithNullable(inputType.IsNullable) - //: throw new InvalidOperationException($"No {nameof(EnumType)} has been created for `{enumType.Name}` {nameof(InputEnumType)}."), - // TODO -- this is temporary until we have support for enums - InputEnumType enumType => CreateCSharpType(enumType.EnumValueType).WithNullable(enumType.IsNullable), + InputLiteralType literalType => CSharpType.FromLiteral(CreateCSharpType(literalType.ValueType), literalType.Value), + InputUnionType unionType => CSharpType.FromUnion(unionType.VariantTypes.Select(CreateCSharpType).ToArray(), unionType.IsNullable), + InputListType { IsEmbeddingsVector: true } listType => new CSharpType(typeof(ReadOnlyMemory<>), listType.IsNullable, CreateCSharpType(listType.ElementType)), + InputListType listType => new CSharpType(typeof(IList<>), listType.IsNullable, CreateCSharpType(listType.ElementType)), + InputDictionaryType dictionaryType => new CSharpType(typeof(IDictionary<,>), inputType.IsNullable, typeof(string), CreateCSharpType(dictionaryType.ValueType)), + InputEnumType enumType => ClientModelPlugin.Instance.OutputLibrary.EnumMappings.TryGetValue(enumType, out var provider) + ? provider.Type.WithNullable(inputType.IsNullable) + : throw new InvalidOperationException($"No {nameof(EnumProvider)} has been created for `{enumType.Name}` {nameof(InputEnumType)}."), InputModelType model => ClientModelPlugin.Instance.OutputLibrary.ModelMappings.TryGetValue(model, out var provider) - ? provider.Type.WithNullable(inputType.IsNullable) - : new CSharpType(typeof(object), model.IsNullable).WithNullable(inputType.IsNullable), + ? provider.Type.WithNullable(inputType.IsNullable) + : new CSharpType(typeof(object), model.IsNullable).WithNullable(inputType.IsNullable), InputPrimitiveType primitiveType => primitiveType.Kind switch { - InputPrimitiveTypeKind.BinaryData => new CSharpType(typeof(BinaryData), inputType.IsNullable), InputPrimitiveTypeKind.Boolean => new CSharpType(typeof(bool), inputType.IsNullable), - InputPrimitiveTypeKind.BytesBase64Url => new CSharpType(typeof(BinaryData), inputType.IsNullable), InputPrimitiveTypeKind.Bytes => new CSharpType(typeof(BinaryData), inputType.IsNullable), - InputPrimitiveTypeKind.ContentType => new CSharpType(typeof(HttpContent), inputType.IsNullable), - InputPrimitiveTypeKind.Date => new CSharpType(typeof(DateTimeOffset), inputType.IsNullable), - InputPrimitiveTypeKind.DateTime => new CSharpType(typeof(DateTimeOffset), inputType.IsNullable), - InputPrimitiveTypeKind.DateTimeISO8601 => new CSharpType(typeof(DateTimeOffset), inputType.IsNullable), - InputPrimitiveTypeKind.DateTimeRFC1123 => new CSharpType(typeof(DateTimeOffset), inputType.IsNullable), - InputPrimitiveTypeKind.DateTimeRFC3339 => new CSharpType(typeof(DateTimeOffset), inputType.IsNullable), - InputPrimitiveTypeKind.DateTimeRFC7231 => new CSharpType(typeof(DateTimeOffset), inputType.IsNullable), - InputPrimitiveTypeKind.DateTimeUnix => new CSharpType(typeof(DateTimeOffset), inputType.IsNullable), + InputPrimitiveTypeKind.ContentType => new CSharpType(typeof(string), inputType.IsNullable), + InputPrimitiveTypeKind.PlainDate => new CSharpType(typeof(DateTimeOffset), inputType.IsNullable), InputPrimitiveTypeKind.Decimal => new CSharpType(typeof(decimal), inputType.IsNullable), InputPrimitiveTypeKind.Decimal128 => new CSharpType(typeof(decimal), inputType.IsNullable), - InputPrimitiveTypeKind.DurationISO8601 => new CSharpType(typeof(TimeSpan), inputType.IsNullable), - InputPrimitiveTypeKind.DurationSeconds => new CSharpType(typeof(TimeSpan), inputType.IsNullable), - InputPrimitiveTypeKind.DurationSecondsFloat => new CSharpType(typeof(TimeSpan), inputType.IsNullable), - InputPrimitiveTypeKind.DurationConstant => new CSharpType(typeof(TimeSpan), inputType.IsNullable), - InputPrimitiveTypeKind.ETag => new CSharpType(typeof(EntityTagHeaderValue), inputType.IsNullable), + InputPrimitiveTypeKind.PlainTime => new CSharpType(typeof(TimeSpan), inputType.IsNullable), InputPrimitiveTypeKind.Float32 => new CSharpType(typeof(float), inputType.IsNullable), InputPrimitiveTypeKind.Float64 => new CSharpType(typeof(double), inputType.IsNullable), InputPrimitiveTypeKind.Float128 => new CSharpType(typeof(decimal), inputType.IsNullable), - InputPrimitiveTypeKind.Guid => new CSharpType(typeof(Guid), inputType.IsNullable), - InputPrimitiveTypeKind.SByte => new CSharpType(typeof(sbyte), inputType.IsNullable), - InputPrimitiveTypeKind.Byte => new CSharpType(typeof(byte), inputType.IsNullable), + InputPrimitiveTypeKind.Guid or InputPrimitiveTypeKind.Uuid => new CSharpType(typeof(Guid), inputType.IsNullable), + InputPrimitiveTypeKind.Int8 => new CSharpType(typeof(sbyte), inputType.IsNullable), + InputPrimitiveTypeKind.UInt8 => new CSharpType(typeof(byte), inputType.IsNullable), InputPrimitiveTypeKind.Int32 => new CSharpType(typeof(int), inputType.IsNullable), InputPrimitiveTypeKind.Int64 => new CSharpType(typeof(long), inputType.IsNullable), InputPrimitiveTypeKind.SafeInt => new CSharpType(typeof(long), inputType.IsNullable), + InputPrimitiveTypeKind.Integer => new CSharpType(typeof(long), inputType.IsNullable), // in typespec, integer is the base type of int related types, see type relation: https://typespec.io/docs/language-basics/type-relations + InputPrimitiveTypeKind.Float => new CSharpType(typeof(double), inputType.IsNullable), // in typespec, float is the base type of float32 and float64, see type relation: https://typespec.io/docs/language-basics/type-relations + InputPrimitiveTypeKind.Numeric => new CSharpType(typeof(double), inputType.IsNullable), // in typespec, numeric is the base type of number types, see type relation: https://typespec.io/docs/language-basics/type-relations InputPrimitiveTypeKind.IPAddress => new CSharpType(typeof(IPAddress), inputType.IsNullable), - InputPrimitiveTypeKind.RequestMethod => new CSharpType(typeof(HttpMethod), inputType.IsNullable), InputPrimitiveTypeKind.Stream => new CSharpType(typeof(Stream), inputType.IsNullable), InputPrimitiveTypeKind.String => new CSharpType(typeof(string), inputType.IsNullable), - InputPrimitiveTypeKind.Time => new CSharpType(typeof(TimeSpan), inputType.IsNullable), - InputPrimitiveTypeKind.Uri => new CSharpType(typeof(Uri), inputType.IsNullable), + InputPrimitiveTypeKind.Uri or InputPrimitiveTypeKind.Url => new CSharpType(typeof(Uri), inputType.IsNullable), + InputPrimitiveTypeKind.Char => new CSharpType(typeof(char), inputType.IsNullable), + InputPrimitiveTypeKind.Any => new CSharpType(typeof(BinaryData), inputType.IsNullable), _ => new CSharpType(typeof(object), inputType.IsNullable), }, - InputGenericType genericType => new CSharpType(genericType.Type, genericType.Arguments.Select(CreateCSharpType).ToArray(), genericType.IsNullable), - InputIntrinsicType { Kind: InputIntrinsicTypeKind.Unknown } => typeof(BinaryData), + InputDateTimeType dateTimeType => new CSharpType(typeof(DateTimeOffset), inputType.IsNullable), + InputDurationType durationType => new CSharpType(typeof(TimeSpan), inputType.IsNullable), _ => throw new Exception("Unknown type") }; - public override Parameter CreateCSharpParam(InputParameter inputParameter) + public override ParameterProvider CreateCSharpParam(InputParameter inputParameter) { - return Parameter.FromInputParameter(inputParameter); + return new ParameterProvider(inputParameter); } /// diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Snippets/SystemExtensibleSnippets.SystemModelSnippets.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Snippets/SystemExtensibleSnippets.SystemModelSnippets.cs new file mode 100644 index 00000000000..7d841407a35 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Snippets/SystemExtensibleSnippets.SystemModelSnippets.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Snippets; +using Microsoft.Generator.CSharp.Statements; + +namespace Microsoft.Generator.CSharp.ClientModel.Snippets +{ + internal partial class SystemExtensibleSnippets + { + internal class SystemModelSnippets : ModelSnippets + { + public override MethodProvider BuildFromOperationResponseMethod(TypeProvider typeProvider, MethodSignatureModifiers modifiers) + { + var result = new ParameterProvider("response", $"The result to deserialize the model from.", typeof(PipelineResponse)); + return new MethodProvider + ( + new MethodSignature(ClientModelPlugin.Instance.Configuration.ApiTypes.FromResponseName, null, $"Deserializes the model from a raw response.", modifiers, typeProvider.Type, null, new[] { result }), + new MethodBodyStatement[] + { + Snippet.UsingVar("document", JsonDocumentSnippet.Parse(new PipelineResponseSnippet(result).Content), out var document), + Snippet.Return(TypeProviderSnippet.Deserialize(typeProvider, document.RootElement)) + }, + "default" + ); + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Snippets/SystemExtensibleSnippets.SystemRestOperationsSnippets.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Snippets/SystemExtensibleSnippets.SystemRestOperationsSnippets.cs new file mode 100644 index 00000000000..5c67e5308ba --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Snippets/SystemExtensibleSnippets.SystemRestOperationsSnippets.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Linq; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Snippets; +using Microsoft.Generator.CSharp.Statements; + +namespace Microsoft.Generator.CSharp.ClientModel.Snippets +{ + internal partial class SystemExtensibleSnippets + { + private class SystemRestOperationsSnippets : RestOperationsSnippets + { + public override StreamSnippet GetContentStream(TypedSnippet result) + => new ClientResultSnippet(result).GetRawResponse().ContentStream; + + public override TypedSnippet GetTypedResponseFromValue(TypedSnippet value, TypedSnippet result) + { + return ClientResultSnippet.FromValue(value, GetRawResponse(result)); + } + + public override TypedSnippet GetTypedResponseFromModel(TypeProvider typeProvider, TypedSnippet result) + { + var response = GetRawResponse(result); + var model = new InvokeStaticMethodExpression(typeProvider.Type, ClientModelPlugin.Instance.Configuration.ApiTypes.FromResponseName, [response]); + return ClientResultSnippet.FromValue(model, response); + } + + public override TypedSnippet GetTypedResponseFromEnum(EnumProvider enumType, TypedSnippet result) + { + var response = GetRawResponse(result); + return ClientResultSnippet.FromValue(enumType.ToEnum(response.Content.ToObjectFromJson(typeof(string))), response); + } + + public override TypedSnippet GetTypedResponseFromBinaryData(Type responseType, TypedSnippet result, string? contentType = null) + { + var rawResponse = GetRawResponse(result); + if (responseType == typeof(string) && contentType != null && FormattableStringHelpers.ToMediaType(contentType) == BodyMediaType.Text) + { + return ClientResultSnippet.FromValue(rawResponse.Content.Untyped.InvokeToString(), rawResponse); + } + return responseType == typeof(BinaryData) + ? ClientResultSnippet.FromValue(rawResponse.Content, rawResponse) + : ClientResultSnippet.FromValue(rawResponse.Content.ToObjectFromJson(responseType), rawResponse); + } + + public override MethodBodyStatement DeclareHttpMessage(MethodSignatureBase createRequestMethodSignature, out TypedSnippet message) + { + var messageVar = new VariableReferenceSnippet(typeof(PipelineMessage), "message"); + message = messageVar; + return Snippet.UsingDeclare(messageVar, new InvokeInstanceMethodExpression(null, createRequestMethodSignature.Name, createRequestMethodSignature.Parameters.Select(p => (ValueExpression)p).ToList(), null, false)); + } + + public override MethodBodyStatement DeclareContentWithUtf8JsonWriter(out TypedSnippet content, out Utf8JsonWriterSnippet writer) + { + var contentVar = new VariableReferenceSnippet(typeof(BinaryContent), "content"); + content = contentVar; + writer = new Utf8JsonWriterSnippet(content.Property("JsonWriter")); + return Snippet.Var(contentVar, Snippet.New.Instance(typeof(BinaryContent))); + } + + private static PipelineResponseSnippet GetRawResponse(TypedSnippet result) + => result.Type.Equals(typeof(PipelineResponse)) + ? new PipelineResponseSnippet(result) + : new ClientResultSnippet(result).GetRawResponse(); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/SystemExtensibleSnippets.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Snippets/SystemExtensibleSnippets.cs similarity index 77% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/SystemExtensibleSnippets.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Snippets/SystemExtensibleSnippets.cs index 98004bbab31..06e44f184f9 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/SystemExtensibleSnippets.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Snippets/SystemExtensibleSnippets.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Snippets; -namespace Microsoft.Generator.CSharp.ClientModel.Expressions +namespace Microsoft.Generator.CSharp.ClientModel.Snippets { internal partial class SystemExtensibleSnippets : ExtensibleSnippets { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Snippets/SystemSnippets.Serializations.JsonFormat.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Snippets/SystemSnippets.Serializations.JsonFormat.cs new file mode 100644 index 00000000000..04c4211927f --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Snippets/SystemSnippets.Serializations.JsonFormat.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Generator.CSharp.Snippets; + +namespace Microsoft.Generator.CSharp.ClientModel.Snippets +{ + internal static partial class SystemSnippets + { + internal static StringSnippet JsonFormatSerialization = Snippet.Literal("J"); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/SystemApiTypes.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/SystemApiTypes.cs index 36c952f087e..f6354de6373 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/SystemApiTypes.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/SystemApiTypes.cs @@ -5,6 +5,7 @@ using System.ClientModel; using System.ClientModel.Primitives; using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Snippets; namespace Microsoft.Generator.CSharp.ClientModel { @@ -75,13 +76,13 @@ public override FormattableString GetSetContentString(string requestName, string public override Type StatusCodeClassifierType => typeof(PipelineMessageClassifier); public override ValueExpression GetCreateFromStreamSampleExpression(ValueExpression freeFormObjectExpression) - => new InvokeStaticMethodExpression(ClientModelPlugin.Instance.Configuration.ApiTypes.RequestContentType, ClientModelPlugin.Instance.Configuration.ApiTypes.RequestContentCreateName, new[] { BinaryDataExpression.FromObjectAsJson(freeFormObjectExpression).ToStream() }); + => new InvokeStaticMethodExpression(ClientModelPlugin.Instance.Configuration.ApiTypes.RequestContentType, ClientModelPlugin.Instance.Configuration.ApiTypes.RequestContentCreateName, [BinaryDataSnippet.FromObjectAsJson(freeFormObjectExpression).ToStream()]); public override string EndPointSampleValue => "https://my-service.com"; public override string JsonElementVariableName => "element"; public override ValueExpression GetKeySampleExpression(string clientName) - => new InvokeStaticMethodExpression(typeof(Environment), nameof(Environment.GetEnvironmentVariable), new[] { new StringLiteralExpression($"{clientName}_KEY", false) }); + => new InvokeStaticMethodExpression(typeof(Environment), nameof(Environment.GetEnvironmentVariable), [Snippet.Literal($"{clientName}_KEY")]); } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/TypedSnippets/ClientResultSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/TypedSnippets/ClientResultSnippet.cs new file mode 100644 index 00000000000..8596cec6675 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/TypedSnippets/ClientResultSnippet.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ClientModel; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Snippets; + +namespace Microsoft.Generator.CSharp.ClientModel.Snippets +{ + internal sealed record ClientResultSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) + { + public ValueExpression Value => Property(nameof(ClientResult.Value)); + public BinaryDataSnippet Content => throw new InvalidOperationException("Result does not have a Content property"); + public StreamSnippet ContentStream => throw new InvalidOperationException("Result does not have a ContentStream property"); + + public static ClientResultSnippet FromResponse(PipelineResponseSnippet response) + => new(InvokeStatic(nameof(ClientResult.FromResponse), response)); + + public static ClientResultSnippet FromValue(ValueExpression value, PipelineResponseSnippet response) + => new(InvokeStatic(nameof(ClientResult.FromValue), value, response)); + + public static ClientResultSnippet FromValue(CSharpType explicitValueType, ValueExpression value, PipelineResponseSnippet response) + => new(new InvokeStaticMethodExpression(typeof(ClientResult), nameof(ClientResult.FromValue), new[] { value, response }, new[] { explicitValueType })); + + public ClientResultSnippet FromValue(ValueExpression value) + => new(new InvokeStaticMethodExpression(typeof(ClientResult), nameof(ClientResult.FromValue), new[] { value, this })); + + public ClientResultSnippet FromValue(CSharpType explicitValueType, ValueExpression value) + => new(new InvokeStaticMethodExpression(typeof(ClientResult), nameof(ClientResult.FromValue), new[] { value, this }, new[] { explicitValueType })); + + public PipelineResponseSnippet GetRawResponse() => new(Untyped.Invoke(nameof(ClientResult.GetRawResponse))); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/TypedSnippets/PipelineMessageSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/TypedSnippets/PipelineMessageSnippet.cs new file mode 100644 index 00000000000..86f26130a0f --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/TypedSnippets/PipelineMessageSnippet.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Snippets; + +namespace Microsoft.Generator.CSharp.ClientModel.Snippets +{ + internal sealed record PipelineMessageSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) + { + public PipelineRequestSnippet Request => new(Property(nameof(PipelineMessage.Request))); + + public PipelineResponseSnippet Response => new(Property(nameof(PipelineMessage.Response))); + + public BoolSnippet BufferResponse => new(Property(nameof(PipelineMessage.BufferResponse))); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/PipelineRequestExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/TypedSnippets/PipelineRequestSnippet.cs similarity index 54% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/PipelineRequestExpression.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/TypedSnippets/PipelineRequestSnippet.cs index 4e10e790340..c03435722ec 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/PipelineRequestExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/TypedSnippets/PipelineRequestSnippet.cs @@ -4,15 +4,17 @@ using System; using System.ClientModel.Primitives; using Microsoft.Generator.CSharp.Expressions; -using static Microsoft.Generator.CSharp.Expressions.Snippets; +using Microsoft.Generator.CSharp.Snippets; +using Microsoft.Generator.CSharp.Statements; +using static Microsoft.Generator.CSharp.Snippets.Snippet; -namespace Microsoft.Generator.CSharp.ClientModel.Expressions +namespace Microsoft.Generator.CSharp.ClientModel.Snippets { - internal sealed record PipelineRequestExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + internal sealed record PipelineRequestSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) { - public TypedValueExpression Uri => new FrameworkTypeExpression(typeof(Uri), Property(nameof(PipelineRequest.Uri))); + public TypedSnippet Uri => new FrameworkTypeSnippet(typeof(Uri), Property(nameof(PipelineRequest.Uri))); public MethodBodyStatement SetMethod(string method) => Assign(Untyped.Property("Method"), Literal(method)); - public MethodBodyStatement SetHeaderValue(string name, StringExpression value) + public MethodBodyStatement SetHeaderValue(string name, StringSnippet value) => new InvokeInstanceMethodStatement(Untyped.Property(nameof(PipelineRequest.Headers)), nameof(PipelineRequestHeaders.Set), Literal(name), value); } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/TypedSnippets/PipelineResponseSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/TypedSnippets/PipelineResponseSnippet.cs new file mode 100644 index 00000000000..35a4ac8980c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/TypedSnippets/PipelineResponseSnippet.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Snippets; + +namespace Microsoft.Generator.CSharp.ClientModel.Snippets +{ + internal sealed record PipelineResponseSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) + { + public BinaryDataSnippet Content => new(Property(nameof(PipelineResponse.Content))); + + public StreamSnippet ContentStream => new(Property(nameof(PipelineResponse.ContentStream))); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/RequestOptionsExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/TypedSnippets/RequestOptionsSnippet.cs similarity index 61% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/RequestOptionsExpression.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/TypedSnippets/RequestOptionsSnippet.cs index fbf45632081..c1c01e53edc 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Expressions/RequestOptionsExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/TypedSnippets/RequestOptionsSnippet.cs @@ -3,13 +3,13 @@ using System.ClientModel.Primitives; using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Snippets; - -namespace Microsoft.Generator.CSharp.ClientModel.Expressions +namespace Microsoft.Generator.CSharp.ClientModel.Snippets { - internal sealed record RequestOptionsExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + internal sealed record RequestOptionsSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) { - public static RequestOptionsExpression FromCancellationToken() + public static RequestOptionsSnippet FromCancellationToken() => new(new InvokeStaticMethodExpression(null, "FromCancellationToken", new ValueExpression[] { KnownParameters.CancellationTokenParameter })); public ValueExpression ErrorOptions => Property(nameof(RequestOptions.ErrorOptions)); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Utilities/StringExtensions.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Utilities/StringExtensions.cs deleted file mode 100644 index bc080fde28f..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/src/Utilities/StringExtensions.cs +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; -using System.Text; -using Microsoft.CodeAnalysis.CSharp; - -namespace Microsoft.Generator.CSharp.ClientModel -{ - internal static class StringExtensions - { - private static bool IsWordSeparator(char c) => !SyntaxFacts.IsIdentifierPartCharacter(c) || c == '_'; - - [return: NotNullIfNotNull("name")] - public static string ToCleanName(this string name, bool isCamelCase = true) - { - if (string.IsNullOrEmpty(name)) - { - return name; - } - StringBuilder nameBuilder = new StringBuilder(); - - int i = 0; - - if (char.IsDigit(name[0])) - { - nameBuilder.Append("_"); - } - else - { - while (!SyntaxFacts.IsIdentifierStartCharacter(name[i])) - { - i++; - } - } - - bool upperCase = false; - int firstWordLength = 1; - for (; i < name.Length; i++) - { - var c = name[i]; - if (IsWordSeparator(c)) - { - upperCase = true; - continue; - } - - if (nameBuilder.Length == 0 && isCamelCase) - { - c = char.ToUpper(c); - upperCase = false; - } - else if (nameBuilder.Length < firstWordLength && !isCamelCase) - { - c = char.ToLower(c); - upperCase = false; - // grow the first word length when this letter follows by two other upper case letters - // this happens in OSProfile, where OS is the first word - if (i + 2 < name.Length && char.IsUpper(name[i + 1]) && (char.IsUpper(name[i + 2]) || IsWordSeparator(name[i + 2]))) - firstWordLength++; - // grow the first word length when this letter follows by another upper case letter and an end of the string - // this happens when the string only has one word, like OS, DNS - if (i + 2 == name.Length && char.IsUpper(name[i + 1])) - firstWordLength++; - } - - if (upperCase) - { - c = char.ToUpper(c); - upperCase = false; - } - - nameBuilder.Append(c); - } - - return nameBuilder.ToString(); - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/MrwSerializationTypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/MrwSerializationTypeProviderTests.cs index fa92d04123e..b181822e6bd 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/MrwSerializationTypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/MrwSerializationTypeProviderTests.cs @@ -1,12 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using Microsoft.CodeAnalysis; +using Microsoft.Generator.CSharp.Expressions; using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Providers; using Moq; using NUnit.Framework; using System; +using System.ClientModel; using System.ClientModel.Primitives; using System.Collections.Generic; +using System.Formats.Asn1; using System.IO; using System.Linq; using System.Reflection; @@ -21,7 +26,9 @@ internal class MrwSerializationTypeProviderTests [SetUp] public void Setup() { - string configFilePath = Path.Combine(AppContext.BaseDirectory, _mocksFolder); + var configFilePath = Path.Combine(AppContext.BaseDirectory, _mocksFolder); + var mockTypeFactory = new Mock() { }; + mockTypeFactory.Setup(t => t.CreateCSharpType(It.IsAny())).Returns(new CSharpType(typeof(int))); // initialize the mock singleton instance of the plugin _mockPlugin = typeof(CodeModelPlugin).GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic); // invoke the load method with the config file path @@ -30,6 +37,7 @@ public void Setup() var config = loadMethod?.Invoke(null, parameters); var mockGeneratorContext = new Mock(config!); var mockPluginInstance = new Mock(mockGeneratorContext.Object) { }; + mockPluginInstance.SetupGet(p => p.TypeFactory).Returns(mockTypeFactory.Object); _mockPlugin?.SetValue(null, mockPluginInstance.Object); } @@ -45,8 +53,8 @@ public void TestBuildImplements() { // mock the model type provider var inputModel = new InputModelType("mockInputModel", "mockNamespace", "public", null, null, InputModelTypeUsage.RoundTrip, Array.Empty(), null, new List(), null, null, null, false); - var mockModelTypeProvider = new ModelTypeProvider(inputModel, null); - var jsonMrwSerializationTypeProvider = new MrwSerializationTypeProvider(mockModelTypeProvider); + var mockModelTypeProvider = new ModelProvider(inputModel); + var jsonMrwSerializationTypeProvider = new MrwSerializationTypeProvider(mockModelTypeProvider, inputModel); var interfaces = jsonMrwSerializationTypeProvider.Implements; Assert.IsNotNull(interfaces); @@ -62,8 +70,8 @@ public void TestBuildImplements() public void TestBuildJsonModelWriteMethod() { var inputModel = new InputModelType("mockInputModel", "mockNamespace", "public", null, null, InputModelTypeUsage.RoundTrip, Array.Empty(), null, new List(), null, null, null, false); - var mockModelTypeProvider = new ModelTypeProvider(inputModel, null); - var jsonMrwSerializationTypeProvider = new MrwSerializationTypeProvider(mockModelTypeProvider); + var mockModelTypeProvider = new ModelProvider(inputModel); + var jsonMrwSerializationTypeProvider = new MrwSerializationTypeProvider(mockModelTypeProvider, inputModel); var method = jsonMrwSerializationTypeProvider.BuildJsonModelWriteMethod(); Assert.IsNotNull(method); @@ -82,8 +90,8 @@ public void TestBuildJsonModelWriteMethod() public void TestBuildJsonModelCreateMethod() { var inputModel = new InputModelType("mockInputModel", "mockNamespace", "public", null, null, InputModelTypeUsage.RoundTrip, Array.Empty(), null, new List(), null, null, null, false); - var mockModelTypeProvider = new ModelTypeProvider(inputModel, null); - var jsonMrwSerializationTypeProvider = new MrwSerializationTypeProvider(mockModelTypeProvider); + var mockModelTypeProvider = new ModelProvider(inputModel); + var jsonMrwSerializationTypeProvider = new MrwSerializationTypeProvider(mockModelTypeProvider, inputModel); var method = jsonMrwSerializationTypeProvider.BuildJsonModelCreateMethod(); Assert.IsNotNull(method); @@ -103,8 +111,8 @@ public void TestBuildJsonModelCreateMethod() public void TestBuildIModelWriteMethodMethod() { var inputModel = new InputModelType("mockInputModel", "mockNamespace", "public", null, null, InputModelTypeUsage.RoundTrip, Array.Empty(), null, new List(), null, null, null, false); - var mockModelTypeProvider = new ModelTypeProvider(inputModel, null); - var jsonMrwSerializationTypeProvider = new MrwSerializationTypeProvider(mockModelTypeProvider); + var mockModelTypeProvider = new ModelProvider(inputModel); + var jsonMrwSerializationTypeProvider = new MrwSerializationTypeProvider(mockModelTypeProvider, inputModel); var method = jsonMrwSerializationTypeProvider.BuildIModelWriteMethod(); Assert.IsNotNull(method); @@ -124,8 +132,8 @@ public void TestBuildIModelWriteMethodMethod() public void TestBuildIModelDeserializationMethod() { var inputModel = new InputModelType("mockInputModel", "mockNamespace", "public", null, null, InputModelTypeUsage.RoundTrip, Array.Empty(), null, new List(), null, null, null, false); - var mockModelTypeProvider = new ModelTypeProvider(inputModel, null); - var jsonMrwSerializationTypeProvider = new MrwSerializationTypeProvider(mockModelTypeProvider); + var mockModelTypeProvider = new ModelProvider(inputModel); + var jsonMrwSerializationTypeProvider = new MrwSerializationTypeProvider(mockModelTypeProvider, inputModel); var method = jsonMrwSerializationTypeProvider.BuildIModelCreateMethod(); Assert.IsNotNull(method); @@ -145,8 +153,8 @@ public void TestBuildIModelDeserializationMethod() public void TestBuildIModelGetFormatMethod() { var inputModel = new InputModelType("mockInputModel", "mockNamespace", "public", null, null, InputModelTypeUsage.RoundTrip, Array.Empty(), null, new List(), null, null, null, false); - var mockModelTypeProvider = new ModelTypeProvider(inputModel, null); - var jsonMrwSerializationTypeProvider = new MrwSerializationTypeProvider(mockModelTypeProvider); + var mockModelTypeProvider = new ModelProvider(inputModel); + var jsonMrwSerializationTypeProvider = new MrwSerializationTypeProvider(mockModelTypeProvider, inputModel); var method = jsonMrwSerializationTypeProvider.BuildIModelGetFormatFromOptionsMethod(); Assert.IsNotNull(method); @@ -163,5 +171,68 @@ public void TestBuildIModelGetFormatMethod() var methodBody = method?.BodyExpression; Assert.IsNotNull(methodBody); } + + [Test] + public void TestBuildSerializationConstructor() + { + var inputModel = new InputModelType("mockInputModel", "mockNamespace", "public", null, null, InputModelTypeUsage.RoundTrip, Array.Empty(), null, new List(), null, null, null, false); + var mockModelTypeProvider = new ModelProvider(inputModel); + var MrwSerializationTypeProvider = new MrwSerializationTypeProvider(mockModelTypeProvider, inputModel); + var constructor = MrwSerializationTypeProvider.BuildSerializationConstructor(); + + Assert.IsNotNull(constructor); + var constructorSignature = constructor?.Signature as ConstructorSignature; + Assert.IsNotNull(constructorSignature); + Assert.AreEqual(1, constructorSignature?.Parameters.Count); + + var param = constructorSignature?.Parameters[0]; + Assert.IsNotNull(param); + Assert.AreEqual("serializedAdditionalRawData", param?.Name); + } + + [Test] + public void TestBuildFields() + { + var inputModel = new InputModelType("mockInputModel", "mockNamespace", "public", null, null, InputModelTypeUsage.RoundTrip, Array.Empty(), null, new List(), null, null, null, false); + var model = new ModelProvider(inputModel); + var typeProvider = new MrwSerializationTypeProvider(model, inputModel); + var fields = typeProvider.Fields; + + // Assert + Assert.IsNotNull(fields); + Assert.AreEqual(1, fields.Count); + Assert.AreEqual("_serializedAdditionalRawData", fields[0].Name); + + var type = fields[0].Type; + Assert.IsTrue(type.IsCollection); + } + + [Test] + public void TestBuildConstructor_ValidateConstructors() + { + var properties = new List{ + new InputModelProperty("requiredString", "requiredString", "", InputPrimitiveType.String, true, false, false), + new InputModelProperty("OptionalInt", "optionalInt", "", InputPrimitiveType.Int32, false, false, false), + new InputModelProperty("requiredCollection", "requiredCollection", "", new InputListType("List", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false, false), true, false, false), + new InputModelProperty("requiredDictionary", "requiredDictionary", "", new InputDictionaryType("Dictionary", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false), true, false, false), + }; + + var inputModel = new InputModelType("TestModel", "TestModel", "public", null, "Test model.", InputModelTypeUsage.RoundTrip, properties, null, Array.Empty(), null, null, null, false); + + var ModelProvider = new ModelProvider(inputModel); + var serializationModelTypeProvider = new MrwSerializationTypeProvider(ModelProvider, inputModel); + var ctors = serializationModelTypeProvider.Constructors; + Assert.IsNotNull(ctors); + + Assert.AreEqual(2, ctors.Count); + + var serializationCtor = ctors[0]; + Assert.AreEqual(MethodSignatureModifiers.Internal, serializationCtor.Signature.Modifiers); + Assert.AreEqual(5, serializationCtor.Signature.Parameters.Count); + + var emptyCtor = ctors[1]; + Assert.AreEqual(MethodSignatureModifiers.Internal, emptyCtor.Signature.Modifiers); + Assert.AreEqual(0, emptyCtor.Signature.Parameters.Count); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/perf/EnumSerialization.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/perf/EnumSerialization.cs new file mode 100644 index 00000000000..64248552e15 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/perf/EnumSerialization.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.IO; +using System.Reflection; +using System.Text.Json; +using BenchmarkDotNet.Attributes; + +namespace Microsoft.Generator.CSharp.Input.Tests.Perf +{ + public class EnumSerialization + { + private static readonly string _path = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location)!.FullName, "TestData", "StringEnum", "tspCodeModel.json"); + private static readonly byte[] _bytes = File.ReadAllBytes(_path); + private TypeSpecReferenceHandler? _resolver; + private JsonSerializerOptions? _options; + + [IterationSetup] + public void Setup() + { + _resolver = new TypeSpecReferenceHandler(); + _options = new JsonSerializerOptions(); + _options.Converters.Add(new TypeSpecInputEnumTypeConverter(_resolver)); + _options.Converters.Add(new TypeSpecInputEnumTypeValueConverter(_resolver)); + } + + [Benchmark] + public void EnumDeserialization() + { + var reader = new Utf8JsonReader(_bytes); + reader.Read(); // { + reader.Read(); // "Enums" + reader.Read(); // [ + reader.Read(); // { + while (reader.TokenType != JsonTokenType.EndArray) + { + if (reader.TokenType == JsonTokenType.StartObject) + { + reader.Read(); + continue; + } + + _ = TypeSpecInputEnumTypeConverter.CreateEnumType(ref reader, null, null, _options!, _resolver!.CurrentResolver); + + if (reader.TokenType == JsonTokenType.EndObject) + { + reader.Read(); + } + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/perf/Microsoft.Generator.CSharp.Input.Tests.Perf.csproj b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/perf/Microsoft.Generator.CSharp.Input.Tests.Perf.csproj new file mode 100644 index 00000000000..59f0888f586 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/perf/Microsoft.Generator.CSharp.Input.Tests.Perf.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + false + Exe + + + + + + + + + + + + + + Always + + + + diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/perf/Program.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/perf/Program.cs new file mode 100644 index 00000000000..83890eee6ba --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/perf/Program.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; +using Perfolizer.Horology; + +namespace Microsoft.Generator.CSharp.Input.Perf; + +public class Program +{ + public static void Main(string[] args) + { + // To see the list of benchmarks that can be run + // dotnet run -c Release --framework net8.0 --list flat + + // To run a specific benchmark class + // dotnet run -c Release --framework net8.0 --filter System.ClientModel.Tests.Internal.Perf.ResourceProviderDataModel* + + // To run a specific benchmark method + // dotnet run -c Release --framework net8.0 --filter *ResourceProviderDataModel.Read_PublicInterface + // or + // dotnet run -c Release --framework net8.0 --filter System.ClientModel.Tests.Internal.Perf.ResourceProviderDataModel.Read_PublicInterface + + // To run a specific benchmark class and category + // dotnet run -c Release --framework net8.0 --anyCategories PublicInterface --filter System.ClientModel.Tests.Internal.Perf.ResourceProviderDataModel* + + var config = ManualConfig.Create(DefaultConfig.Instance); + config.Options = ConfigOptions.JoinSummary | ConfigOptions.StopOnFirstError; + config = config.AddDiagnoser(MemoryDiagnoser.Default); + config.AddJob(Job.Default + .WithWarmupCount(1) + .WithIterationTime(TimeInterval.FromMilliseconds(250)) + .WithMinIterationCount(15) + .WithMaxIterationCount(20) + .AsDefault()); + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/perf/TestData/StringEnum/tspCodeModel.json b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/perf/TestData/StringEnum/tspCodeModel.json new file mode 100644 index 00000000000..48b4d1c121c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/perf/TestData/StringEnum/tspCodeModel.json @@ -0,0 +1,60 @@ +{ + "Enums": [ + { + "$id": "14", + "Kind": "Enum", + "Name": "StringFixedEnum", + "EnumValueType": "String", + "AllowedValues": [ + { + "$id": "15", + "Name": "One", + "Value": "1" + }, + { + "$id": "16", + "Name": "Two", + "Value": "2" + }, + { + "$id": "17", + "Name": "Four", + "Value": "4" + } + ], + "Namespace": "", + "Description": "Simple enum", + "IsExtensible": false, + "IsNullable": true, + "Usage": "RoundTrip" + }, + { + "$id": "18", + "Kind": "Enum", + "Name": "StringExtensibleEnum", + "EnumValueType": "String", + "AllowedValues": [ + { + "$id": "19", + "Name": "One", + "Value": "1" + }, + { + "$id": "20", + "Name": "Two", + "Value": "2" + }, + { + "$id": "21", + "Name": "Four", + "Value": "4" + } + ], + "Namespace": "", + "Description": "Extensible enum", + "IsExtensible": true, + "IsNullable": true, + "Usage": "RoundTrip" + } + ] +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputLibrary.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputLibrary.cs index 2aeebaf5e30..031fcc5fd10 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputLibrary.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputLibrary.cs @@ -20,7 +20,7 @@ public InputLibrary(string codeModelPath) private InputNamespace? _inputNamespace; public InputNamespace InputNamespace => _inputNamespace ??= Load(_codeModelPath); - public InputNamespace Load(string outputDirectory) + internal InputNamespace Load(string outputDirectory) { var codeModelFile = Path.Combine(outputDirectory, CodeModelInputFileName); if (!File.Exists(codeModelFile)) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/BytesKnownEncoding.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/BytesKnownEncoding.cs new file mode 100644 index 00000000000..14b181f694b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/BytesKnownEncoding.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Generator.CSharp.Input +{ + public static class BytesKnownEncoding + { + public const string Base64 = "base64"; + public const string Base64Url = "base64url"; + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/DateTimeKnownEncoding.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/DateTimeKnownEncoding.cs new file mode 100644 index 00000000000..8662932072e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/DateTimeKnownEncoding.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Generator.CSharp.Input +{ + public enum DateTimeKnownEncoding + { + Rfc3339, Rfc7231, UnixTimestamp + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/DurationKnownEncoding.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/DurationKnownEncoding.cs new file mode 100644 index 00000000000..45c52d46233 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/DurationKnownEncoding.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Generator.CSharp.Input +{ + public enum DurationKnownEncoding + { + Iso8601, Seconds, Constant + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/ExampleMockValueBuilder.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/ExampleMockValueBuilder.cs index 2230a5da25f..129e119c348 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/ExampleMockValueBuilder.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/ExampleMockValueBuilder.cs @@ -63,11 +63,16 @@ private static InputParameterExample BuildParameterExample(InputParameter parame // when it is constant, it could have DefaultValue value = InputExampleValue.Value(parameter.Type, parameter.DefaultValue.Value); } - else if (parameter.Type is InputUnionType unionType && unionType.UnionItemTypes.First() is InputLiteralType literalType) + else if (parameter.Type is InputUnionType unionType && unionType.VariantTypes[0] is InputLiteralType literalType) { // or it could be a union of literal types value = InputExampleValue.Value(parameter.Type, literalType.Value); } + else if (parameter.Type is InputEnumType enumType && enumType.Values[0].Value is { } enumValue) + { + // or it could be an enum of a few values + value = InputExampleValue.Value(parameter.Type, enumValue); + } else { // fallback to null @@ -95,24 +100,26 @@ private static InputParameterExample BuildParameterExample(InputParameter parame private static InputExampleValue BuildExampleValue(InputType type, string? hint, bool useAllParameters, HashSet visitedModels) => type switch { - InputList listType => BuildListExampleValue(listType, hint, useAllParameters, visitedModels), - InputDictionary dictionaryType => BuildDictionaryExampleValue(dictionaryType, hint, useAllParameters, visitedModels), + InputListType listType => BuildListExampleValue(listType, hint, useAllParameters, visitedModels), + InputDictionaryType dictionaryType => BuildDictionaryExampleValue(dictionaryType, hint, useAllParameters, visitedModels), InputEnumType enumType => BuildEnumExampleValue(enumType), InputPrimitiveType primitiveType => BuildPrimitiveExampleValue(primitiveType, hint), InputLiteralType literalType => InputExampleValue.Value(literalType, literalType.Value), InputModelType modelType => BuildModelExampleValue(modelType, useAllParameters, visitedModels), - InputUnionType unionType => BuildExampleValue(unionType.UnionItemTypes.First(), hint, useAllParameters, visitedModels), + InputUnionType unionType => BuildExampleValue(unionType.VariantTypes[0], hint, useAllParameters, visitedModels), + InputDateTimeType dateTimeType => BuildDateTimeExampleValue(dateTimeType), + InputDurationType durationType => BuildDurationExampleValue(durationType), _ => InputExampleValue.Object(type, new Dictionary()) }; - private static InputExampleValue BuildListExampleValue(InputList listType, string? hint, bool useAllParameters, HashSet visitedModels) + private static InputExampleValue BuildListExampleValue(InputListType listType, string? hint, bool useAllParameters, HashSet visitedModels) { var exampleElementValue = BuildExampleValue(listType.ElementType, hint, useAllParameters, visitedModels); return InputExampleValue.List(listType, new[] { exampleElementValue }); } - private static InputExampleValue BuildDictionaryExampleValue(InputDictionary dictionaryType, string? hint, bool useAllParameters, HashSet visitedModels) + private static InputExampleValue BuildDictionaryExampleValue(InputDictionaryType dictionaryType, string? hint, bool useAllParameters, HashSet visitedModels) { var exampleValue = BuildExampleValue(dictionaryType.ValueType, hint, useAllParameters, visitedModels); @@ -124,7 +131,7 @@ private static InputExampleValue BuildDictionaryExampleValue(InputDictionary dic private static InputExampleValue BuildEnumExampleValue(InputEnumType enumType) { - var enumValue = enumType.AllowedValues.First(); + var enumValue = enumType.Values[0]; return InputExampleValue.Value(enumType, enumValue.Value); } @@ -132,29 +139,42 @@ private static InputExampleValue BuildEnumExampleValue(InputEnumType enumType) { InputPrimitiveTypeKind.Stream => InputExampleValue.Stream(primitiveType, ""), InputPrimitiveTypeKind.Boolean => InputExampleValue.Value(primitiveType, true), - InputPrimitiveTypeKind.Date => InputExampleValue.Value(primitiveType, "2022-05-10"), - InputPrimitiveTypeKind.DateTime => InputExampleValue.Value(primitiveType, "2022-05-10T14:57:31.2311892-04:00"), - InputPrimitiveTypeKind.DateTimeISO8601 => InputExampleValue.Value(primitiveType, "2022-05-10T18:57:31.2311892Z"), - InputPrimitiveTypeKind.DateTimeRFC1123 => InputExampleValue.Value(primitiveType, "Tue, 10 May 2022 18:57:31 GMT"), - InputPrimitiveTypeKind.DateTimeRFC3339 => InputExampleValue.Value(primitiveType, "2022-05-10T18:57:31.2311892Z"), - InputPrimitiveTypeKind.DateTimeRFC7231 => InputExampleValue.Value(primitiveType, "Tue, 10 May 2022 18:57:31 GMT"), - InputPrimitiveTypeKind.DateTimeUnix => InputExampleValue.Value(primitiveType, 1652209051), + InputPrimitiveTypeKind.PlainDate => InputExampleValue.Value(primitiveType, "2022-05-10"), InputPrimitiveTypeKind.Float32 => InputExampleValue.Value(primitiveType, 123.45f), InputPrimitiveTypeKind.Float64 => InputExampleValue.Value(primitiveType, 123.45d), InputPrimitiveTypeKind.Float128 => InputExampleValue.Value(primitiveType, 123.45m), - InputPrimitiveTypeKind.Guid => InputExampleValue.Value(primitiveType, "73f411fe-4f43-4b4b-9cbd-6828d8f4cf9a"), + InputPrimitiveTypeKind.Guid or InputPrimitiveTypeKind.Uuid => InputExampleValue.Value(primitiveType, "73f411fe-4f43-4b4b-9cbd-6828d8f4cf9a"), + InputPrimitiveTypeKind.Int8 => InputExampleValue.Value(primitiveType, (sbyte)123), + InputPrimitiveTypeKind.UInt8 => InputExampleValue.Value(primitiveType, (byte)123), InputPrimitiveTypeKind.Int32 => InputExampleValue.Value(primitiveType, 1234), InputPrimitiveTypeKind.Int64 => InputExampleValue.Value(primitiveType, 1234L), + InputPrimitiveTypeKind.SafeInt => InputExampleValue.Value(primitiveType, 1234L), InputPrimitiveTypeKind.String => string.IsNullOrWhiteSpace(hint) ? InputExampleValue.Value(primitiveType, "") : InputExampleValue.Value(primitiveType, $"<{hint}>"), - InputPrimitiveTypeKind.DurationISO8601 => InputExampleValue.Value(primitiveType, "PT1H23M45S"), - InputPrimitiveTypeKind.DurationConstant => InputExampleValue.Value(primitiveType, "01:23:45"), - InputPrimitiveTypeKind.Time => InputExampleValue.Value(primitiveType, "01:23:45"), - InputPrimitiveTypeKind.Uri => InputExampleValue.Value(primitiveType, "http://localhost:3000"), - InputPrimitiveTypeKind.DurationSeconds => InputExampleValue.Value(primitiveType, 10), - InputPrimitiveTypeKind.DurationSecondsFloat => InputExampleValue.Value(primitiveType, 10f), + InputPrimitiveTypeKind.PlainTime => InputExampleValue.Value(primitiveType, "01:23:45"), + InputPrimitiveTypeKind.Uri or InputPrimitiveTypeKind.Url => InputExampleValue.Value(primitiveType, "http://localhost:3000"), _ => InputExampleValue.Object(primitiveType, new Dictionary()) }; + private static InputExampleValue BuildDateTimeExampleValue(InputDateTimeType dateTimeType) => dateTimeType.Encode switch + { + DateTimeKnownEncoding.Rfc7231 => InputExampleValue.Value(dateTimeType.WireType, "Tue, 10 May 2022 18:57:31 GMT"), + DateTimeKnownEncoding.Rfc3339 => InputExampleValue.Value(dateTimeType.WireType, "2022-05-10T18:57:31.2311892Z"), + DateTimeKnownEncoding.UnixTimestamp => InputExampleValue.Value(dateTimeType.WireType, 1652209051), + _ => InputExampleValue.Null(dateTimeType) + }; + + private static InputExampleValue BuildDurationExampleValue(InputDurationType durationType) => durationType.Encode switch + { + DurationKnownEncoding.Iso8601 => InputExampleValue.Value(durationType.WireType, "PT1H23M45S"), + DurationKnownEncoding.Seconds => durationType.WireType.Kind switch + { + InputPrimitiveTypeKind.Int32 => InputExampleValue.Value(durationType.WireType, 10), + InputPrimitiveTypeKind.Float or InputPrimitiveTypeKind.Float32 => InputExampleValue.Value(durationType.WireType, 10f), + _ => InputExampleValue.Value(durationType.WireType, 3.141592) + }, + _ => InputExampleValue.Null(durationType) + }; + private static InputExampleValue BuildModelExampleValue(InputModelType model, bool useAllParameters, HashSet visitedModels) { if (visitedModels.Contains(model)) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputDateTimeType.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputDateTimeType.cs new file mode 100644 index 00000000000..c1ee4d7ab7b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputDateTimeType.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Generator.CSharp.Input +{ + public class InputDateTimeType : InputType + { + public InputDateTimeType(DateTimeKnownEncoding encode, InputPrimitiveType wireType, bool isNullable) : base("DateTime", isNullable) + { + Encode = encode; + WireType = wireType; + } + + public DateTimeKnownEncoding Encode { get; } + public InputPrimitiveType WireType { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputDictionary.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputDictionaryType.cs similarity index 79% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputDictionary.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputDictionaryType.cs index c3729115734..d655d64d667 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputDictionary.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputDictionaryType.cs @@ -6,14 +6,14 @@ namespace Microsoft.Generator.CSharp.Input /// /// Represents an input dictionary type. /// - public sealed class InputDictionary : InputType + public sealed class InputDictionaryType : InputType { - /// Creates an instance of . + /// Creates an instance of . /// The name of the dictionary. /// The key's . /// The value's . /// Flag used to determine if the input dictionary type is nullable. - public InputDictionary(string name, InputType keyType, InputType valueType, bool isNullable) : base(name, isNullable) + public InputDictionaryType(string name, InputType keyType, InputType valueType, bool isNullable) : base(name, isNullable) { KeyType = keyType; ValueType = valueType; diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputDurationType.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputDurationType.cs new file mode 100644 index 00000000000..7455e2f88e7 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputDurationType.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Generator.CSharp.Input +{ + public class InputDurationType : InputType + { + public InputDurationType(DurationKnownEncoding encode, InputPrimitiveType wireType, bool isNullable) : base("Duration", isNullable) + { + Encode = encode; + WireType = wireType; + } + + public DurationKnownEncoding Encode { get; } + public InputPrimitiveType WireType { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputEnumType.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputEnumType.cs index 544aefaad1f..e95f1736cd9 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputEnumType.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputEnumType.cs @@ -9,7 +9,7 @@ namespace Microsoft.Generator.CSharp.Input { public class InputEnumType : InputType { - public InputEnumType(string name, string? enumNamespace, string? accessibility, string? deprecated, string description, InputModelTypeUsage usage, InputPrimitiveType enumValueType, IReadOnlyList allowedValues, bool isExtensible, bool isNullable) + public InputEnumType(string name, string? enumNamespace, string? accessibility, string? deprecated, string description, InputModelTypeUsage usage, InputPrimitiveType valueType, IReadOnlyList values, bool isExtensible, bool isNullable) : base(name, isNullable) { Namespace = enumNamespace; @@ -17,8 +17,8 @@ public InputEnumType(string name, string? enumNamespace, string? accessibility, Deprecated = deprecated; Description = description; Usage = usage; - EnumValueType = enumValueType; - AllowedValues = allowedValues; + ValueType = valueType; + Values = values; IsExtensible = isExtensible; } @@ -27,8 +27,8 @@ public InputEnumType(string name, string? enumNamespace, string? accessibility, public string? Deprecated { get; } public string Description { get; } public InputModelTypeUsage Usage { get; } - public InputPrimitiveType EnumValueType { get; } - public IReadOnlyList AllowedValues { get; } + public InputPrimitiveType ValueType { get; } + public IReadOnlyList Values { get; } public bool IsExtensible { get; } public static IEqualityComparer IgnoreNullabilityComparer { get; } = new IgnoreNullabilityComparerImplementation(); @@ -51,8 +51,8 @@ public bool Equals(InputEnumType? x, InputEnumType? y) && x.Namespace == y.Namespace && x.Accessibility == y.Accessibility && x.Description == y.Description - && x.EnumValueType.Equals(y.EnumValueType) - && x.AllowedValues.SequenceEqual(y.AllowedValues) + && x.ValueType.Equals(y.ValueType) + && x.Values.SequenceEqual(y.Values) && x.IsExtensible == y.IsExtensible; } @@ -63,9 +63,9 @@ public int GetHashCode(InputEnumType obj) hashCode.Add(obj.Namespace); hashCode.Add(obj.Accessibility); hashCode.Add(obj.Description); - hashCode.Add(obj.EnumValueType); + hashCode.Add(obj.ValueType); hashCode.Add(obj.IsExtensible); - foreach (var item in obj.AllowedValues) + foreach (var item in obj.Values) { hashCode.Add(item); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputGenericType.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputGenericType.cs deleted file mode 100644 index d93b5308fcd..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputGenericType.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; - -namespace Microsoft.Generator.CSharp.Input -{ - public class InputGenericType : InputType - { - public InputGenericType(Type type, IReadOnlyList arguments, bool isNullable) : base(type.Name, isNullable) - { - Type = type; - Arguments = arguments; - } - - public Type Type { get; } - public IReadOnlyList Arguments { get; } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputIntrinsicType.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputIntrinsicType.cs deleted file mode 100644 index 13a80088c7a..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputIntrinsicType.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Input -{ - // TODO -- after we have adopted getAllModels in our emitter, we no longer need to have this type. https://github.com/microsoft/typespec/issues/3338 - // The only thing we need in these "intrinsic types" is the "unknown", and TCGC put it in the primitive type. Others we will never generate therefore we do not need to have others - public class InputIntrinsicType : InputType - { - public InputIntrinsicType(InputIntrinsicTypeKind kind) : base(kind.ToString(), false) - { - Kind = kind; - } - - public InputIntrinsicTypeKind Kind { get; } - - public static InputIntrinsicType ErrorType { get; } = new(InputIntrinsicTypeKind.ErrorType); - public static InputIntrinsicType Void { get; } = new(InputIntrinsicTypeKind.Void); - public static InputIntrinsicType Never { get; } = new(InputIntrinsicTypeKind.Never); - public static InputIntrinsicType Unknown { get; } = new(InputIntrinsicTypeKind.Unknown); - public static InputIntrinsicType Null { get; } = new(InputIntrinsicTypeKind.Null); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputIntrinsicTypeKind.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputIntrinsicTypeKind.cs deleted file mode 100644 index c1d98450e37..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputIntrinsicTypeKind.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Input -{ - public enum InputIntrinsicTypeKind - { - ErrorType, - Void, - Never, - Unknown, - Null, - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputList.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputListType.cs similarity index 81% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputList.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputListType.cs index 5873470b369..d79f864380f 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputList.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputListType.cs @@ -6,14 +6,14 @@ namespace Microsoft.Generator.CSharp.Input /// /// Represents an input list type. /// - public sealed class InputList : InputType + public sealed class InputListType : InputType { - /// Creates an instance of . + /// Creates an instance of . /// The name of the list type. /// The element's . /// Flag used to determine if the input list type is embedding vector. /// Flag used to determine if the input list type is nullable. - public InputList(string name, InputType elementType, bool isEmbeddingsVector, bool isNullable) : base(name, isNullable) + public InputListType(string name, InputType elementType, bool isEmbeddingsVector, bool isNullable) : base(name, isNullable) { ElementType = elementType; IsEmbeddingsVector = isEmbeddingsVector; diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputLiteralType.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputLiteralType.cs index 9c59a8a63b9..b153e3f96cf 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputLiteralType.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputLiteralType.cs @@ -5,13 +5,13 @@ namespace Microsoft.Generator.CSharp.Input { public sealed class InputLiteralType : InputType { - public InputLiteralType(string name, InputType literalValueType, object value, bool isNullable) : base(name, isNullable) + public InputLiteralType(InputType valueType, object value, bool isNullable) : base("Literal", isNullable) { - LiteralValueType = literalValueType; + ValueType = valueType; Value = value; } - public InputType LiteralValueType { get; } + public InputType ValueType { get; } public object Value { get; } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputModelProperty.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputModelProperty.cs index c09ba02c91f..32891685f96 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputModelProperty.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputModelProperty.cs @@ -1,11 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; +using System.Collections.Generic; + namespace Microsoft.Generator.CSharp.Input { public class InputModelProperty { - public InputModelProperty(string name, string serializedName, string description, InputType type, bool isRequired, bool isReadOnly, bool isDiscriminator) + public InputModelProperty(string name, string serializedName, string description, InputType type, bool isRequired, bool isReadOnly, bool isDiscriminator, IReadOnlyList? flattenedNames = null) { Name = name; SerializedName = serializedName; @@ -14,6 +17,7 @@ public InputModelProperty(string name, string serializedName, string description IsRequired = isRequired; IsReadOnly = isReadOnly; IsDiscriminator = isDiscriminator; + FlattenedNames = flattenedNames ?? Array.Empty(); } public string Name { get; } @@ -23,5 +27,6 @@ public InputModelProperty(string name, string serializedName, string description public bool IsRequired { get; } public bool IsReadOnly { get; } public bool IsDiscriminator { get; } + public IReadOnlyList FlattenedNames { get; } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputModelType.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputModelType.cs index e96de832c05..6adb1eaea16 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputModelType.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputModelType.cs @@ -9,7 +9,7 @@ namespace Microsoft.Generator.CSharp.Input [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] public class InputModelType : InputType { - public InputModelType(string name, string? modelNamespace, string? accessibility, string? deprecated, string? description, InputModelTypeUsage usage, IReadOnlyList properties, InputModelType? baseModel, IReadOnlyList derivedModels, string? discriminatorValue, string? discriminatorPropertyName, InputDictionary? inheritedDictionaryType, bool isNullable) + public InputModelType(string name, string? modelNamespace, string? accessibility, string? deprecated, string? description, InputModelTypeUsage usage, IReadOnlyList properties, InputModelType? baseModel, IReadOnlyList derivedModels, string? discriminatorValue, string? discriminatorPropertyName, InputDictionaryType? inheritedDictionaryType, bool isNullable) : base(name, isNullable) { Namespace = modelNamespace; @@ -37,7 +37,7 @@ public InputModelType(string name, string? modelNamespace, string? accessibility public IReadOnlyList DerivedModels { get; internal set; } public string? DiscriminatorValue { get; internal set; } public string? DiscriminatorPropertyName { get; internal set; } - public InputDictionary? InheritedDictionaryType { get; internal set; } + public InputDictionaryType? InheritedDictionaryType { get; internal set; } public bool IsUnknownDiscriminatorModel { get; init; } public bool IsPropertyBag { get; init; } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputNamespace.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputNamespace.cs index 5f442ce46c1..f25e6d44c31 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputNamespace.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputNamespace.cs @@ -8,10 +8,9 @@ namespace Microsoft.Generator.CSharp.Input { public class InputNamespace { - public InputNamespace(string name, string description, IReadOnlyList apiVersions, IReadOnlyList enums, IReadOnlyList models, IReadOnlyList clients, InputAuth auth) + public InputNamespace(string name, IReadOnlyList apiVersions, IReadOnlyList enums, IReadOnlyList models, IReadOnlyList clients, InputAuth auth) { Name = name; - Description = description; ApiVersions = apiVersions; Enums = enums; Models = models; @@ -19,10 +18,9 @@ public InputNamespace(string name, string description, IReadOnlyList api Auth = auth; } - public InputNamespace() : this(name: string.Empty, description: string.Empty, apiVersions: Array.Empty(), enums: Array.Empty(), models: Array.Empty(), clients: Array.Empty(), auth: new InputAuth()) { } + public InputNamespace() : this(name: string.Empty, apiVersions: Array.Empty(), enums: Array.Empty(), models: Array.Empty(), clients: Array.Empty(), auth: new InputAuth()) { } public string Name { get; init; } - public string Description { get; init; } public IReadOnlyList ApiVersions { get; init; } public IReadOnlyList Enums { get; init; } public IReadOnlyList Models { get; init; } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputParameter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputParameter.cs index 9e7d48c6ea2..6a4b8ff9208 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputParameter.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputParameter.cs @@ -43,26 +43,6 @@ public InputParameter( HeaderCollectionPrefix = headerCollectionPrefix; } - public InputParameter() : this( - name: string.Empty, - nameInRequest: string.Empty, - description: null, - type: InputPrimitiveType.Object, - location: RequestLocation.None, - defaultValue: null, - groupedBy: null, - kind: InputOperationParameterKind.Method, - isRequired: false, - isApiVersion: false, - isResourceParameter: false, - isContentType: false, - isEndpoint: false, - skipUrlEncoding: false, - explode: false, - arraySerializationDelimiter: null, - headerCollectionPrefix: null) - { } - public string Name { get; } public string NameInRequest { get; } public string? Description { get; } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputPrimitiveType.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputPrimitiveType.cs index fbf0df66e3e..123615043af 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputPrimitiveType.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputPrimitiveType.cs @@ -10,23 +10,20 @@ public InputPrimitiveType(InputPrimitiveTypeKind kind, bool isNullable = false) Kind = kind; } + public InputPrimitiveType(InputPrimitiveTypeKind kind, string? encode, bool isNullable = false) : this(kind, isNullable) + { + Encode = encode; + } + public InputPrimitiveTypeKind Kind { get; } + public string? Encode { get; } - public static InputPrimitiveType BinaryData { get; } = new(InputPrimitiveTypeKind.BinaryData); public static InputPrimitiveType Boolean { get; } = new(InputPrimitiveTypeKind.Boolean); - public static InputPrimitiveType Bytes { get; } = new(InputPrimitiveTypeKind.Bytes); - public static InputPrimitiveType BytesBase64Url { get; } = new(InputPrimitiveTypeKind.BytesBase64Url); + public static InputPrimitiveType Base64 { get; } = new(InputPrimitiveTypeKind.Bytes, BytesKnownEncoding.Base64, false); + public static InputPrimitiveType Base64Url { get; } = new(InputPrimitiveTypeKind.Bytes, BytesKnownEncoding.Base64Url, false); + public static InputPrimitiveType Char { get; } = new(InputPrimitiveTypeKind.Char); public static InputPrimitiveType ContentType { get; } = new(InputPrimitiveTypeKind.ContentType); - public static InputPrimitiveType Date { get; } = new(InputPrimitiveTypeKind.Date); - public static InputPrimitiveType DateTime { get; } = new(InputPrimitiveTypeKind.DateTime); - public static InputPrimitiveType DateTimeISO8601 { get; } = new(InputPrimitiveTypeKind.DateTimeISO8601); - public static InputPrimitiveType DateTimeRFC1123 { get; } = new(InputPrimitiveTypeKind.DateTimeRFC1123); - public static InputPrimitiveType DateTimeRFC3339 { get; } = new(InputPrimitiveTypeKind.DateTimeRFC3339); - public static InputPrimitiveType DateTimeRFC7231 { get; } = new(InputPrimitiveTypeKind.DateTimeRFC7231); - public static InputPrimitiveType DateTimeUnix { get; } = new(InputPrimitiveTypeKind.DateTimeUnix); - public static InputPrimitiveType DurationISO8601 { get; } = new(InputPrimitiveTypeKind.DurationISO8601); - public static InputPrimitiveType DurationConstant { get; } = new(InputPrimitiveTypeKind.DurationConstant); - public static InputPrimitiveType ETag { get; } = new(InputPrimitiveTypeKind.ETag); + public static InputPrimitiveType PlainDate { get; } = new(InputPrimitiveTypeKind.PlainDate); public static InputPrimitiveType Float32 { get; } = new(InputPrimitiveTypeKind.Float32); public static InputPrimitiveType Float64 { get; } = new(InputPrimitiveTypeKind.Float64); public static InputPrimitiveType Float128 { get; } = new(InputPrimitiveTypeKind.Float128); @@ -34,14 +31,11 @@ public InputPrimitiveType(InputPrimitiveTypeKind kind, bool isNullable = false) public static InputPrimitiveType Int32 { get; } = new(InputPrimitiveTypeKind.Int32); public static InputPrimitiveType Int64 { get; } = new(InputPrimitiveTypeKind.Int64); public static InputPrimitiveType IPAddress { get; } = new(InputPrimitiveTypeKind.IPAddress); - public static InputPrimitiveType Object { get; } = new(InputPrimitiveTypeKind.Object); - public static InputPrimitiveType RequestMethod { get; } = new(InputPrimitiveTypeKind.RequestMethod); - public static InputPrimitiveType ResourceIdentifier { get; } = new(InputPrimitiveTypeKind.ResourceIdentifier); - public static InputPrimitiveType ResourceType { get; } = new(InputPrimitiveTypeKind.ResourceType); public static InputPrimitiveType Stream { get; } = new(InputPrimitiveTypeKind.Stream); public static InputPrimitiveType String { get; } = new(InputPrimitiveTypeKind.String); - public static InputPrimitiveType Time { get; } = new(InputPrimitiveTypeKind.Time); + public static InputPrimitiveType PlainTime { get; } = new(InputPrimitiveTypeKind.PlainTime); public static InputPrimitiveType Uri { get; } = new(InputPrimitiveTypeKind.Uri); + public static InputPrimitiveType Any { get; } = new(InputPrimitiveTypeKind.Any); public bool IsNumber => Kind is InputPrimitiveTypeKind.Int32 or InputPrimitiveTypeKind.Int64 or InputPrimitiveTypeKind.Float32 or InputPrimitiveTypeKind.Float64 or InputPrimitiveTypeKind.Float128; } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputPrimitiveTypeKind.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputPrimitiveTypeKind.cs index ad8a219bce4..c86aa880118 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputPrimitiveTypeKind.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputPrimitiveTypeKind.cs @@ -6,41 +6,35 @@ namespace Microsoft.Generator.CSharp.Input public enum InputPrimitiveTypeKind { Boolean, - BinaryData, Bytes, - BytesBase64Url, ContentType, - Date, - DateTime, - DateTimeISO8601, - DateTimeRFC1123, - DateTimeRFC3339, - DateTimeRFC7231, - DateTimeUnix, + PlainDate, Decimal, Decimal128, - DurationISO8601, - DurationConstant, - DurationSeconds, - DurationSecondsFloat, - ETag, + Numeric,// in typespec, numeric is the base type of all number types, see type relation: https://typespec.io/docs/language-basics/type-relations + Float, // in typespec, float is the base type of float32 and float64, see type relation: https://typespec.io/docs/language-basics/type-relations Float32, Float64, Float128, Guid, + Uuid, + Integer, // in typespec, integer is the base type of int related types, see type relation: https://typespec.io/docs/language-basics/type-relations + Int8, // aka SByte + Int16, Int32, Int64, SafeInt, + UInt8, // aka Byte + UInt16, + UInt32, + UInt64, IPAddress, - Object, - RequestMethod, - ResourceIdentifier, - ResourceType, Stream, String, - Time, + PlainTime, + Url, Uri, - SByte, - Byte + Char, + Any // aka unknown } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputType.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputType.cs index 49738286a34..5a581aa5311 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputType.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputType.cs @@ -26,14 +26,14 @@ internal InputType GetCollectionEquivalent(InputType inputType) { switch (this) { - case InputList listType: - return new InputList( + case InputListType listType: + return new InputListType( listType.Name, listType.ElementType.GetCollectionEquivalent(inputType), listType.IsEmbeddingsVector, listType.IsNullable); - case InputDictionary dictionaryType: - return new InputDictionary( + case InputDictionaryType dictionaryType: + return new InputDictionaryType( dictionaryType.Name, dictionaryType.KeyType, dictionaryType.ValueType.GetCollectionEquivalent(inputType), diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputUnionType.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputUnionType.cs index 4f27f894c5c..916e54ec928 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputUnionType.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/InputUnionType.cs @@ -7,11 +7,11 @@ namespace Microsoft.Generator.CSharp.Input { public class InputUnionType : InputType { - public InputUnionType(string name, IReadOnlyList unionItemTypes, bool isNullable) : base(name, isNullable) + public InputUnionType(string name, IReadOnlyList variantTypes, bool isNullable) : base(name, isNullable) { - UnionItemTypes = unionItemTypes; + VariantTypes = variantTypes; } - public IReadOnlyList UnionItemTypes { get; } + public IReadOnlyList VariantTypes { get; internal set; } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/SerializationFormat.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/SerializationFormat.cs index 157174b79de..8ca0ea09733 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/SerializationFormat.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/SerializationFormat.cs @@ -19,6 +19,7 @@ public enum SerializationFormat Duration_Constant, Duration_Seconds, Duration_Seconds_Float, + Duration_Seconds_Double, Time_ISO8601, Bytes_Base64Url, Bytes_Base64 diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputConstantConverter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputConstantConverter.cs index cf48a0cd136..1cf7b03bc7f 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputConstantConverter.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputConstantConverter.cs @@ -90,7 +90,7 @@ public static object ReadConstantValue(ref Utf8JsonReader reader, string propert } break; case InputEnumType enumType: - switch (enumType.EnumValueType.Kind) + switch (enumType.ValueType.Kind) { case InputPrimitiveTypeKind.String: value = reader.GetString() ?? throw new JsonException(); @@ -102,7 +102,7 @@ public static object ReadConstantValue(ref Utf8JsonReader reader, string propert value = reader.GetDouble(); break; default: - throw new JsonException($"Unsupported enum value type: {enumType.EnumValueType.Kind}"); + throw new JsonException($"Unsupported enum value type: {enumType.ValueType.Kind}"); } break; case InputLiteralType literalType: diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputDateTimeTypeConverter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputDateTimeTypeConverter.cs new file mode 100644 index 00000000000..985d14ea23e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputDateTimeTypeConverter.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Generator.CSharp.Input +{ + internal class TypeSpecInputDateTimeTypeConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + public TypeSpecInputDateTimeTypeConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputDateTimeType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateDateTimeType(ref reader, null, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputDateTimeType value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + public static InputDateTimeType CreateDateTimeType(ref Utf8JsonReader reader, string? id, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = id == null; + bool isNullable = false; + string? encode = null; + InputType? type = null; + + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadBoolean(nameof(InputDateTimeType.IsNullable), ref isNullable) + || reader.TryReadString(nameof(InputDateTimeType.Encode), ref encode) + || reader.TryReadWithConverter(nameof(InputDateTimeType.WireType), options, ref type); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + if (type is not InputPrimitiveType wireType) + { + throw new JsonException("The wireType of a DateTime type must be a primitive type"); + } + + encode = encode ?? throw new JsonException("DateTime type must have encoding"); + + var dateTimeType = Enum.TryParse(encode, ignoreCase: true, out var encodeKind) + ? new InputDateTimeType(encodeKind, wireType, isNullable) + : throw new JsonException($"Encoding of DateTime type {encode} is unknown."); + + if (id != null) + { + resolver.AddReference(id, dateTimeType); + } + return dateTimeType; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputDictionaryTypeConverter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputDictionaryTypeConverter.cs index 18a15a6c346..eca28d21c09 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputDictionaryTypeConverter.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputDictionaryTypeConverter.cs @@ -7,7 +7,7 @@ namespace Microsoft.Generator.CSharp.Input { - internal sealed class TypeSpecInputDictionaryTypeConverter : JsonConverter + internal sealed class TypeSpecInputDictionaryTypeConverter : JsonConverter { private readonly TypeSpecReferenceHandler _referenceHandler; @@ -16,13 +16,13 @@ public TypeSpecInputDictionaryTypeConverter(TypeSpecReferenceHandler referenceHa _referenceHandler = referenceHandler; } - public override InputDictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateDictionaryType(ref reader, null, null, options, _referenceHandler.CurrentResolver); + public override InputDictionaryType? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateDictionaryType(ref reader, null, null, options, _referenceHandler.CurrentResolver); - public override void Write(Utf8JsonWriter writer, InputDictionary value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, InputDictionaryType value, JsonSerializerOptions options) => throw new NotSupportedException("Writing not supported"); - public static InputDictionary CreateDictionaryType(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) + public static InputDictionaryType CreateDictionaryType(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) { var isFirstProperty = id == null && name == null; bool isNullable = false; @@ -31,10 +31,10 @@ public static InputDictionary CreateDictionaryType(ref Utf8JsonReader reader, st while (reader.TokenType != JsonTokenType.EndObject) { var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) - || reader.TryReadString(nameof(InputDictionary.Name), ref name) - || reader.TryReadBoolean(nameof(InputDictionary.IsNullable), ref isNullable) - || reader.TryReadWithConverter(nameof(InputDictionary.KeyType), options, ref keyType) - || reader.TryReadWithConverter(nameof(InputDictionary.ValueType), options, ref valueType); + || reader.TryReadString(nameof(InputDictionaryType.Name), ref name) + || reader.TryReadBoolean(nameof(InputDictionaryType.IsNullable), ref isNullable) + || reader.TryReadWithConverter(nameof(InputDictionaryType.KeyType), options, ref keyType) + || reader.TryReadWithConverter(nameof(InputDictionaryType.ValueType), options, ref valueType); if (!isKnownProperty) { @@ -46,7 +46,7 @@ public static InputDictionary CreateDictionaryType(ref Utf8JsonReader reader, st keyType = keyType ?? throw new JsonException("Dictionary must have key type"); valueType = valueType ?? throw new JsonException("Dictionary must have value type"); - var dictType = new InputDictionary(name, keyType, valueType, isNullable); + var dictType = new InputDictionaryType(name, keyType, valueType, isNullable); if (id != null) { resolver.AddReference(id, dictType); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputDurationTypeConverter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputDurationTypeConverter.cs new file mode 100644 index 00000000000..b966d7dcbfc --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputDurationTypeConverter.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Generator.CSharp.Input +{ + internal class TypeSpecInputDurationTypeConverter : JsonConverter + { + private readonly TypeSpecReferenceHandler _referenceHandler; + public TypeSpecInputDurationTypeConverter(TypeSpecReferenceHandler referenceHandler) + { + _referenceHandler = referenceHandler; + } + + public override InputDurationType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateDurationType(ref reader, null, options, _referenceHandler.CurrentResolver); + + public override void Write(Utf8JsonWriter writer, InputDurationType value, JsonSerializerOptions options) + => throw new NotSupportedException("Writing not supported"); + + public static InputDurationType CreateDurationType(ref Utf8JsonReader reader, string? id, JsonSerializerOptions options, ReferenceResolver resolver) + { + var isFirstProperty = id == null; + bool isNullable = false; + string? encode = null; + InputType? type = null; + + while (reader.TokenType != JsonTokenType.EndObject) + { + var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) + || reader.TryReadBoolean(nameof(InputDurationType.IsNullable), ref isNullable) + || reader.TryReadString(nameof(InputDurationType.Encode), ref encode) + || reader.TryReadWithConverter(nameof(InputDurationType.WireType), options, ref type); + + if (!isKnownProperty) + { + reader.SkipProperty(); + } + } + + if (type is not InputPrimitiveType wireType) + { + throw new JsonException("The wireType of a Duration type must be a primitive type"); + } + + encode = encode ?? throw new JsonException("Duration type must have encoding"); + + var dateTimeType = Enum.TryParse(encode, ignoreCase: true, out var encodeKind) + ? new InputDurationType(encodeKind, wireType, isNullable) + : throw new JsonException($"Encoding of Duration type {encode} is unknown."); + + if (id != null) + { + resolver.AddReference(id, dateTimeType); + } + return dateTimeType; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputEnumTypeConverter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputEnumTypeConverter.cs index 4a85cf6288c..1f0e444c588 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputEnumTypeConverter.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputEnumTypeConverter.cs @@ -34,8 +34,8 @@ public static InputEnumType CreateEnumType(ref Utf8JsonReader reader, string? id InputModelTypeUsage usage = InputModelTypeUsage.None; string? usageString = null; bool isExtendable = false; - InputPrimitiveType? valueType = null; - IReadOnlyList? allowedValues = null; + InputType? valueType = null; + IReadOnlyList? values = null; while (reader.TokenType != JsonTokenType.EndObject) { var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) @@ -47,8 +47,8 @@ public static InputEnumType CreateEnumType(ref Utf8JsonReader reader, string? id || reader.TryReadString(nameof(InputEnumType.Description), ref description) || reader.TryReadString(nameof(InputEnumType.Usage), ref usageString) || reader.TryReadBoolean(nameof(InputEnumType.IsExtensible), ref isExtendable) - || reader.TryReadPrimitiveType(nameof(InputEnumType.EnumValueType), ref valueType) - || reader.TryReadWithConverter(nameof(InputEnumType.AllowedValues), options, ref allowedValues); + || reader.TryReadWithConverter(nameof(InputEnumType.ValueType), options, ref valueType) + || reader.TryReadWithConverter(nameof(InputEnumType.Values), options, ref values); if (!isKnownProperty) { @@ -68,14 +68,17 @@ public static InputEnumType CreateEnumType(ref Utf8JsonReader reader, string? id Enum.TryParse(usageString, ignoreCase: true, out usage); } - if (allowedValues == null || allowedValues.Count == 0) + if (values == null || values.Count == 0) { throw new JsonException("Enum must have at least one value"); } - valueType = valueType ?? throw new JsonException("Enum value type must be set."); + if (valueType is not InputPrimitiveType inputValueType) + { + throw new JsonException("The ValueType of an EnumType must be a primitive type."); + } - var enumType = new InputEnumType(name, ns, accessibility, deprecated, description, usage, valueType, NormalizeValues(allowedValues, valueType), isExtendable, isNullable); + var enumType = new InputEnumType(name, ns, accessibility, deprecated, description!, usage, inputValueType, NormalizeValues(values, inputValueType), isExtendable, isNullable); if (id != null) { resolver.AddReference(id, enumType); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputListTypeConverter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputListTypeConverter.cs index 6968be99747..5d60ae6dd1b 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputListTypeConverter.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputListTypeConverter.cs @@ -7,7 +7,7 @@ namespace Microsoft.Generator.CSharp.Input { - internal sealed class TypeSpecInputListTypeConverter : JsonConverter + internal sealed class TypeSpecInputListTypeConverter : JsonConverter { private readonly TypeSpecReferenceHandler _referenceHandler; @@ -16,13 +16,13 @@ public TypeSpecInputListTypeConverter(TypeSpecReferenceHandler referenceHandler) _referenceHandler = referenceHandler; } - public override InputList? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateListType(ref reader, null, null, options, _referenceHandler.CurrentResolver); + public override InputListType? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateListType(ref reader, null, null, options, _referenceHandler.CurrentResolver); - public override void Write(Utf8JsonWriter writer, InputList value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, InputListType value, JsonSerializerOptions options) => throw new NotSupportedException("Writing not supported"); - public static InputList CreateListType(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) + public static InputListType CreateListType(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) { var isFirstProperty = id == null && name == null; bool isNullable = false; @@ -30,9 +30,9 @@ public static InputList CreateListType(ref Utf8JsonReader reader, string? id, st while (reader.TokenType != JsonTokenType.EndObject) { var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) - || reader.TryReadString(nameof(InputList.Name), ref name) - || reader.TryReadBoolean(nameof(InputList.IsNullable), ref isNullable) - || reader.TryReadWithConverter(nameof(InputList.ElementType), options, ref elementType); + || reader.TryReadString(nameof(InputListType.Name), ref name) + || reader.TryReadBoolean(nameof(InputListType.IsNullable), ref isNullable) + || reader.TryReadWithConverter(nameof(InputListType.ElementType), options, ref elementType); if (!isKnownProperty) { @@ -41,7 +41,7 @@ public static InputList CreateListType(ref Utf8JsonReader reader, string? id, st } elementType = elementType ?? throw new JsonException("List must have element type"); - var listType = new InputList(name ?? "List", elementType, false, isNullable); + var listType = new InputListType(name ?? "List", elementType, false, isNullable); if (id != null) { resolver.AddReference(id, listType); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputLiteralTypeConverter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputLiteralTypeConverter.cs index 6dab4e3a521..79444e4f23c 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputLiteralTypeConverter.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputLiteralTypeConverter.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -31,9 +31,8 @@ public static InputLiteralType CreateInputLiteralType(ref Utf8JsonReader reader, while (reader.TokenType != JsonTokenType.EndObject) { var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) - || reader.TryReadString(nameof(InputLiteralType.Name), ref name) || reader.TryReadBoolean(nameof(InputLiteralType.IsNullable), ref isNullable) - || reader.TryReadWithConverter(nameof(InputLiteralType.LiteralValueType), options, ref type); + || reader.TryReadWithConverter(nameof(InputLiteralType.ValueType), options, ref type); if (isKnownProperty) { @@ -50,13 +49,11 @@ public static InputLiteralType CreateInputLiteralType(ref Utf8JsonReader reader, } } - name = name ?? throw new JsonException($"{nameof(InputLiteralType)} must have a name."); - type = type ?? throw new JsonException("InputConstant must have type"); value = value ?? throw new JsonException("InputConstant must have value"); - var literalType = new InputLiteralType(name, type, value, isNullable); + var literalType = new InputLiteralType(type, value, isNullable); if (id != null) { @@ -86,7 +83,7 @@ public static object ReadLiteralValue(ref Utf8JsonReader reader, string property var kind = type switch { InputPrimitiveType primitiveType => primitiveType.Kind, - InputEnumType enumType => enumType.EnumValueType.Kind, + InputEnumType enumType => enumType.ValueType.Kind, _ => throw new JsonException($"Not supported literal type {type.GetType()}.") }; object value = kind switch diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputModelPropertyConverter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputModelPropertyConverter.cs index b4193bcb83f..6798277326f 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputModelPropertyConverter.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputModelPropertyConverter.cs @@ -1,7 +1,8 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; @@ -31,6 +32,7 @@ private static InputModelProperty ReadInputModelProperty(ref Utf8JsonReader read bool isReadOnly = false; bool isRequired = false; bool isDiscriminator = false; + IReadOnlyList? flattenedNames = null; while (reader.TokenType != JsonTokenType.EndObject) { @@ -41,7 +43,8 @@ private static InputModelProperty ReadInputModelProperty(ref Utf8JsonReader read || reader.TryReadWithConverter(nameof(InputModelProperty.Type), options, ref propertyType) || reader.TryReadBoolean(nameof(InputModelProperty.IsReadOnly), ref isReadOnly) || reader.TryReadBoolean(nameof(InputModelProperty.IsRequired), ref isRequired) - || reader.TryReadBoolean(nameof(InputModelProperty.IsDiscriminator), ref isDiscriminator); + || reader.TryReadBoolean(nameof(InputModelProperty.IsDiscriminator), ref isDiscriminator) + || reader.TryReadWithConverter(nameof(InputModelProperty.FlattenedNames), options, ref flattenedNames); if (!isKnownProperty) { @@ -55,7 +58,7 @@ private static InputModelProperty ReadInputModelProperty(ref Utf8JsonReader read // description = BuilderHelpers.EscapeXmlDocDescription(description); propertyType = propertyType ?? throw new JsonException($"{nameof(InputModelProperty)} must have a property type."); - var property = new InputModelProperty(name, serializedName ?? name, description, propertyType, isRequired, isReadOnly, isDiscriminator); + var property = new InputModelProperty(name, serializedName ?? name, description, propertyType, isRequired, isReadOnly, isDiscriminator, flattenedNames); if (id != null) { resolver.AddReference(id, property); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputModelTypeConverter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputModelTypeConverter.cs index 1e0b8c31d12..8a7ab3f21d0 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputModelTypeConverter.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputModelTypeConverter.cs @@ -32,19 +32,8 @@ public static InputModelType CreateModelType(ref Utf8JsonReader reader, string? id = id ?? throw new JsonException(); - // skip every other property until we have a name - while (name == null) - { - var hasName = reader.TryReadString(nameof(InputModelType.Name), ref name); - if (!hasName) - { - reader.SkipProperty(); - } - } - name = name ?? throw new JsonException("Model must have name"); - // create an empty model to resolve circular references - var model = new InputModelType(name, null, null, null, null, InputModelTypeUsage.None, null!, null, new List(), null, null, null, false); + var model = new InputModelType(name!, null, null, null, null, InputModelTypeUsage.None, null!, null, new List(), null, null, null, false); resolver.AddReference(id, model); bool isNullable = false; @@ -55,14 +44,15 @@ public static InputModelType CreateModelType(ref Utf8JsonReader reader, string? string? usageString = null; string? discriminatorPropertyName = null; string? discriminatorValue = null; - InputDictionary? inheritedDictionaryType = null; + InputDictionaryType? inheritedDictionaryType = null; InputModelType? baseModel = null; IReadOnlyList? properties = null; // read all possible properties and throw away the unknown properties while (reader.TokenType != JsonTokenType.EndObject) { - var isKnownProperty = reader.TryReadBoolean(nameof(InputModelType.IsNullable), ref isNullable) + var isKnownProperty = reader.TryReadString(nameof(InputModelType.Name), ref name) + || reader.TryReadBoolean(nameof(InputModelType.IsNullable), ref isNullable) || reader.TryReadString(nameof(InputModelType.Namespace), ref ns) || reader.TryReadString(nameof(InputModelType.Accessibility), ref accessibility) || reader.TryReadString(nameof(InputModelType.Deprecated), ref deprecated) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputNamespaceConverter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputNamespaceConverter.cs index 5a24626fb7e..3d927eee66a 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputNamespaceConverter.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputNamespaceConverter.cs @@ -31,7 +31,6 @@ public override void Write(Utf8JsonWriter writer, InputNamespace value, JsonSeri } string? name = null; - string? description = null; IReadOnlyList? apiVersions = null; IReadOnlyList? enums = null; IReadOnlyList? models = null; @@ -41,7 +40,6 @@ public override void Write(Utf8JsonWriter writer, InputNamespace value, JsonSeri while (reader.TokenType != JsonTokenType.EndObject) { var isKnownProperty = reader.TryReadString(nameof(InputNamespace.Name), ref name) - || reader.TryReadString(nameof(InputNamespace.Description), ref description) || reader.TryReadWithConverter(nameof(InputNamespace.ApiVersions), options, ref apiVersions) || reader.TryReadWithConverter(nameof(InputNamespace.Enums), options, ref enums) || reader.TryReadWithConverter(nameof(InputNamespace.Models), options, ref models) @@ -70,7 +68,6 @@ public override void Write(Utf8JsonWriter writer, InputNamespace value, JsonSeri return new InputNamespace( name ?? throw new JsonException(), - description ?? throw new JsonException(), apiVersions, enums, models, diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputTypeConverter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputTypeConverter.cs index c4f7a8b176c..590f5d63403 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputTypeConverter.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputTypeConverter.cs @@ -20,11 +20,6 @@ public TypeSpecInputTypeConverter(TypeSpecReferenceHandler referenceHandler) public override InputType? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonTokenType.String) - { - return CreatePrimitiveType(reader.GetString(), false); - } - return reader.ReadReferenceAndResolve(_referenceHandler.CurrentResolver) ?? CreateObject(ref reader, options); } @@ -54,38 +49,41 @@ private InputType CreateObject(ref Utf8JsonReader reader, JsonSerializerOptions return result ?? throw new JsonException("cannot deserialize InputType"); } - private const string PrimitiveKind = "Primitive"; - private const string LiteralKind = "Literal"; - private const string UnionKind = "Union"; + private const string LiteralKind = "constant"; + private const string UnionKind = "union"; private const string ModelKind = "Model"; - private const string EnumKind = "Enum"; + private const string EnumKind = "enum"; private const string ArrayKind = "Array"; private const string DictionaryKind = "Dictionary"; - private const string IntrinsicKind = "Intrinsic"; + private const string UtcDateTimeKind = "utcDateTime"; + private const string OffsetDateTimeKind = "offsetDateTime"; + private const string DurationKind = "duration"; private InputType CreateDerivedType(ref Utf8JsonReader reader, string? id, string? kind, string? name, JsonSerializerOptions options) => kind switch { - PrimitiveKind => ReadPrimitiveType(ref reader, id, name, _referenceHandler.CurrentResolver), + null => throw new JsonException("InputType must have a 'Kind' property"), LiteralKind => TypeSpecInputLiteralTypeConverter.CreateInputLiteralType(ref reader, id, name, options, _referenceHandler.CurrentResolver), UnionKind => TypeSpecInputUnionTypeConverter.CreateInputUnionType(ref reader, id, name, options, _referenceHandler.CurrentResolver), ModelKind => TypeSpecInputModelTypeConverter.CreateModelType(ref reader, id, name, options, _referenceHandler.CurrentResolver), EnumKind => TypeSpecInputEnumTypeConverter.CreateEnumType(ref reader, id, name, options, _referenceHandler.CurrentResolver), ArrayKind => TypeSpecInputListTypeConverter.CreateListType(ref reader, id, name, options, _referenceHandler.CurrentResolver), DictionaryKind => TypeSpecInputDictionaryTypeConverter.CreateDictionaryType(ref reader, id, name, options, _referenceHandler.CurrentResolver), - IntrinsicKind => ReadIntrinsicType(ref reader, id, name, _referenceHandler.CurrentResolver), - null => throw new JsonException("InputType must have a 'Kind' property"), - _ => throw new JsonException($"unknown kind {kind}") + UtcDateTimeKind or OffsetDateTimeKind => TypeSpecInputDateTimeTypeConverter.CreateDateTimeType(ref reader, id, options, _referenceHandler.CurrentResolver), + DurationKind => TypeSpecInputDurationTypeConverter.CreateDurationType(ref reader, id, options, _referenceHandler.CurrentResolver), + _ => ReadPrimitiveType(ref reader, id, kind, _referenceHandler.CurrentResolver), }; - public static InputPrimitiveType ReadPrimitiveType(ref Utf8JsonReader reader, string? id, string? name, ReferenceResolver resolver) + private static InputPrimitiveType ReadPrimitiveType(ref Utf8JsonReader reader, string? id, string? kind, ReferenceResolver resolver) { var isFirstProperty = id == null; var isNullable = false; + string? encode = null; while (reader.TokenType != JsonTokenType.EndObject) { var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) || reader.TryReadBoolean(nameof(InputPrimitiveType.IsNullable), ref isNullable) - || reader.TryReadString(nameof(InputPrimitiveType.Name), ref name); // the primitive kind in the json is represented by the property `Name`. + || reader.TryReadString(nameof(InputPrimitiveType.Kind), ref kind) + || reader.TryReadString(nameof(InputPrimitiveType.Encode), ref encode); if (!isKnownProperty) { @@ -93,7 +91,7 @@ public static InputPrimitiveType ReadPrimitiveType(ref Utf8JsonReader reader, st } } - var primitiveType = CreatePrimitiveType(name, isNullable); + var primitiveType = CreatePrimitiveType(kind, encode, isNullable); if (id != null) { resolver.AddReference(id, primitiveType); @@ -102,43 +100,13 @@ public static InputPrimitiveType ReadPrimitiveType(ref Utf8JsonReader reader, st return primitiveType; } - public static InputPrimitiveType CreatePrimitiveType(string? inputTypeKindString, bool isNullable) + public static InputPrimitiveType CreatePrimitiveType(string? primitiveKind, string? encode, bool isNullable) { - ArgumentNullException.ThrowIfNull(inputTypeKindString, nameof(inputTypeKindString)); - return Enum.TryParse(inputTypeKindString, ignoreCase: true, out var kind) - ? new InputPrimitiveType(kind, isNullable) - : throw new JsonException($"{inputTypeKindString} type is unknown."); - } + ArgumentNullException.ThrowIfNull(primitiveKind, nameof(primitiveKind)); - private static InputIntrinsicType ReadIntrinsicType(ref Utf8JsonReader reader, string? id, string? name, ReferenceResolver resolver) - { - var isFirstProperty = id == null; - while (reader.TokenType != JsonTokenType.EndObject) - { - var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) - || reader.TryReadString(nameof(InputIntrinsicType.Kind), ref name); // the InputIntrinsicType kind in the json is represented by the property `Name`. - - if (!isKnownProperty) - { - reader.SkipProperty(); - } - } - - var intrinsicType = CreateIntrinsicType(name); - if (id != null) - { - resolver.AddReference(id, intrinsicType); - } - - return intrinsicType; - } - - private static InputIntrinsicType CreateIntrinsicType(string? inputTypeKindString) - { - ArgumentNullException.ThrowIfNull(inputTypeKindString, nameof(inputTypeKindString)); - return Enum.TryParse(inputTypeKindString, ignoreCase: true, out var kind) - ? new InputIntrinsicType(kind) - : throw new InvalidOperationException($"{inputTypeKindString} type is unknown for InputIntrinsicType."); + return Enum.TryParse(primitiveKind, ignoreCase: true, out var kind) + ? new InputPrimitiveType(kind, encode, isNullable) + : throw new JsonException($"{primitiveKind} type is unknown."); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputUnionTypeConverter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputUnionTypeConverter.cs index d7203f812d0..c6159934a16 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputUnionTypeConverter.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecInputUnionTypeConverter.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -24,59 +24,39 @@ public override void Write(Utf8JsonWriter writer, InputUnionType value, JsonSeri public static InputUnionType CreateInputUnionType(ref Utf8JsonReader reader, string? id, string? name, JsonSerializerOptions options, ReferenceResolver resolver) { - var isFirstProperty = id == null; + if (id == null) + { + reader.TryReadReferenceId(ref id); + } + + id = id ?? throw new JsonException(); + + // create an empty model to resolve circular references + var union = new InputUnionType(null!, null!, false); + resolver.AddReference(id, union); + bool isNullable = false; - var unionItemTypes = new List(); + IReadOnlyList? variantTypes = null; while (reader.TokenType != JsonTokenType.EndObject) { - var isKnownProperty = reader.TryReadReferenceId(ref isFirstProperty, ref id) - || reader.TryReadString(nameof(InputUnionType.Name), ref name) + var isKnownProperty = reader.TryReadString(nameof(InputUnionType.Name), ref name) + || reader.TryReadWithConverter(nameof(InputUnionType.VariantTypes), options, ref variantTypes) || reader.TryReadBoolean(nameof(InputUnionType.IsNullable), ref isNullable); - if (isKnownProperty) - { - continue; - } - - if (reader.GetString() == nameof(InputUnionType.UnionItemTypes)) - { - reader.Read(); - CreateUnionItemTypes(ref reader, unionItemTypes, options); - } - else + if (!isKnownProperty) { reader.SkipProperty(); } } - name = name ?? throw new JsonException($"{nameof(InputLiteralType)} must have a name."); - if (unionItemTypes == null || unionItemTypes.Count == 0) + union.Name = name ?? throw new JsonException($"{nameof(InputLiteralType)} must have a name."); + if (variantTypes == null || variantTypes.Count == 0) { throw new JsonException("Union must have a least one union type"); } - - var unionType = new InputUnionType(name, unionItemTypes, isNullable); - if (id != null) - { - resolver.AddReference(id, unionType); - } - return unionType; - } - - private static void CreateUnionItemTypes(ref Utf8JsonReader reader, ICollection itemTypes, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartArray) - { - throw new JsonException(); - } - reader.Read(); - - while (reader.TokenType != JsonTokenType.EndArray) - { - var type = reader.ReadWithConverter(options); - itemTypes.Add(type ?? throw new JsonException($"null {nameof(InputType)} isn't allowed")); - } - reader.Read(); + union.VariantTypes = variantTypes; + union.IsNullable = isNullable; + return union; } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecSerialization.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecSerialization.cs index 7171563dc1c..b46bebe0094 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecSerialization.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/TypeSpecSerialization.cs @@ -31,7 +31,9 @@ public static class TypeSpecSerialization new TypeSpecInputUnionTypeConverter(referenceHandler), new TypeSpecInputClientConverter(referenceHandler), new TypeSpecInputOperationConverter(referenceHandler), - new TypeSpecInputParameterConverter(referenceHandler) + new TypeSpecInputParameterConverter(referenceHandler), + new TypeSpecInputDateTimeTypeConverter(referenceHandler), + new TypeSpecInputDurationTypeConverter(referenceHandler) } }; diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/Utf8JsonReaderExtensions.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/Utf8JsonReaderExtensions.cs index 1ab03eea8d2..898a298c949 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/Utf8JsonReaderExtensions.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/InputTypes/Serialization/Utf8JsonReaderExtensions.cs @@ -129,24 +129,6 @@ public static bool TryReadEnumValue(this ref Utf8JsonReader reader, string prope return true; } - public static bool TryReadPrimitiveType(this ref Utf8JsonReader reader, string propertyName, ref InputPrimitiveType? value) - { - if (reader.TokenType != JsonTokenType.PropertyName) - { - throw new JsonException(); - } - - if (reader.GetString() != propertyName) - { - return false; - } - - reader.Read(); - value = TypeSpecInputTypeConverter.CreatePrimitiveType(reader.GetString(), false) ?? throw new JsonException(); - reader.Read(); - return true; - } - public static bool TryReadWithConverter(this ref Utf8JsonReader reader, string propertyName, JsonSerializerOptions options, ref T? value) { if (reader.TokenType != JsonTokenType.PropertyName) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/Properties/AssemblyInfo.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..5fc1a715abe --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.Input/src/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.Generator.CSharp.Input.Tests.Perf")] +[assembly: InternalsVisibleTo("Microsoft.Generator.CSharp.Input.Tests")] diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.sln b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.sln index 481eeeef874..5d8e30804df 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.sln +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.sln @@ -1,5 +1,4 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.8.34309.116 MinimumVisualStudioVersion = 10.0.40219.1 @@ -23,7 +22,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestProjects.Local.Tests", "TestProjects\Local\TestProjects.Local.Tests.csproj", "{BFE39EDA-ADA1-4F60-A47C-ADE88C67B3B7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Generator.CSharp.ClientModel.Tests", "Microsoft.Generator.CSharp.ClientModel\test\Microsoft.Generator.CSharp.ClientModel.Tests.csproj", "{D30EC838-B0A0-44CD-A361-239FF256A176}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Generator.CSharp.ClientModel.Tests", "Microsoft.Generator.CSharp.ClientModel\test\Microsoft.Generator.CSharp.ClientModel.Tests.csproj", "{D30EC838-B0A0-44CD-A361-239FF256A176}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Generator.CSharp.Input.Tests.Perf", "Microsoft.Generator.CSharp.Input\perf\Microsoft.Generator.CSharp.Input.Tests.Perf.csproj", "{DF2E7916-D0C4-47CA-876A-5B78A3A3FD77}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Generator.CSharp.Tests.Perf", "Microsoft.Generator.CSharp\perf\Microsoft.Generator.CSharp.Tests.Perf.csproj", "{8AE5D726-610C-4A72-B2A9-5C53C423C485}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -63,6 +66,14 @@ Global {D30EC838-B0A0-44CD-A361-239FF256A176}.Debug|Any CPU.Build.0 = Debug|Any CPU {D30EC838-B0A0-44CD-A361-239FF256A176}.Release|Any CPU.ActiveCfg = Release|Any CPU {D30EC838-B0A0-44CD-A361-239FF256A176}.Release|Any CPU.Build.0 = Release|Any CPU + {DF2E7916-D0C4-47CA-876A-5B78A3A3FD77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF2E7916-D0C4-47CA-876A-5B78A3A3FD77}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF2E7916-D0C4-47CA-876A-5B78A3A3FD77}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF2E7916-D0C4-47CA-876A-5B78A3A3FD77}.Release|Any CPU.Build.0 = Release|Any CPU + {8AE5D726-610C-4A72-B2A9-5C53C423C485}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8AE5D726-610C-4A72-B2A9-5C53C423C485}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8AE5D726-610C-4A72-B2A9-5C53C423C485}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8AE5D726-610C-4A72-B2A9-5C53C423C485}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/perf/CodeWriterBenchmark.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/perf/CodeWriterBenchmark.cs new file mode 100644 index 00000000000..5a3fd737db0 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/perf/CodeWriterBenchmark.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Reflection; +using BenchmarkDotNet.Attributes; +using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Providers; + +namespace Microsoft.Generator.CSharp.Perf +{ + public class CodeWriterBenchmark + { + private TypeProviderWriter _writer; + + public CodeWriterBenchmark() + { + PluginHandler pluginHandler = new PluginHandler(); + pluginHandler.LoadPlugin(Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location)!.FullName, "Projects", "Model")); + + var properties = new[] + { + new InputModelProperty("MyProperty", "myProperty", "The property of mine", new InputPrimitiveType(InputPrimitiveTypeKind.Int32, false), true, false, false) + }; + var inputModel = new InputModelType("MyModel", null, null, null, "Test model", InputModelTypeUsage.RoundTrip, properties, null, Array.Empty(), null, null, null, false); + var modelProvider = new ModelProvider(inputModel); + _writer = new TypeProviderWriter(modelProvider); + } + + [Benchmark] + public void WriteModel() + { + _writer.Write(); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/perf/Microsoft.Generator.CSharp.Tests.Perf.csproj b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/perf/Microsoft.Generator.CSharp.Tests.Perf.csproj new file mode 100644 index 00000000000..f054daeaf39 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/perf/Microsoft.Generator.CSharp.Tests.Perf.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + false + Exe + + + + + + + + + + + + + + + Always + + + Always + + + + diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/perf/Program.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/perf/Program.cs new file mode 100644 index 00000000000..c4c94a31b20 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/perf/Program.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; +using Perfolizer.Horology; + +namespace Microsoft.Generator.CSharp.Perf; + +public class Program +{ + public static void Main(string[] args) + { + // To see the list of benchmarks that can be run + // dotnet run -c Release --framework net8.0 --list flat + + // To run a specific benchmark class + // dotnet run -c Release --framework net8.0 --filter System.ClientModel.Tests.Internal.Perf.ResourceProviderDataModel* + + // To run a specific benchmark method + // dotnet run -c Release --framework net8.0 --filter *ResourceProviderDataModel.Read_PublicInterface + // or + // dotnet run -c Release --framework net8.0 --filter System.ClientModel.Tests.Internal.Perf.ResourceProviderDataModel.Read_PublicInterface + + // To run a specific benchmark class and category + // dotnet run -c Release --framework net8.0 --anyCategories PublicInterface --filter System.ClientModel.Tests.Internal.Perf.ResourceProviderDataModel* + + var config = ManualConfig.Create(DefaultConfig.Instance); + config.Options = ConfigOptions.JoinSummary | ConfigOptions.StopOnFirstError; + config = config.AddDiagnoser(MemoryDiagnoser.Default); + config.AddJob(Job.Default + .WithWarmupCount(1) + .WithIterationTime(TimeInterval.FromMilliseconds(250)) + .WithMinIterationCount(15) + .WithMaxIterationCount(20) + .AsDefault()); + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/perf/Projects/Model/Configuration.json b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/perf/Projects/Model/Configuration.json new file mode 100644 index 00000000000..166195e5f8d --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/perf/Projects/Model/Configuration.json @@ -0,0 +1,6 @@ +{ + "output-folder": ".", + "namespace": "UnbrandedTypeSpec", + "library-name": "UnbrandedTypeSpec", + "use-model-reader-writer": true +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/perf/Projects/Model/tspCodeModel.json b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/perf/Projects/Model/tspCodeModel.json new file mode 100644 index 00000000000..06a7d278edd --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/perf/Projects/Model/tspCodeModel.json @@ -0,0 +1,2852 @@ +{ + "$id": "1", + "Name": "UnbrandedTypeSpec", + "ApiVersions": [], + "Enums": [ + { + "$id": "2", + "Kind": "enum", + "Name": "Thing_requiredLiteralString", + "ValueType": { + "$id": "3", + "Kind": "string", + "IsNullable": false + }, + "Values": [ + { + "$id": "4", + "Name": "accept", + "Value": "accept", + "Description": "accept" + } + ], + "Namespace": "UnbrandedTypeSpec", + "Description": "The Thing_requiredLiteralString", + "IsExtensible": true, + "IsNullable": false, + "Usage": "RoundTrip" + }, + { + "$id": "5", + "Kind": "enum", + "Name": "Thing_requiredLiteralInt", + "ValueType": { + "$id": "6", + "Kind": "int32", + "IsNullable": false + }, + "Values": [ + { + "$id": "7", + "Name": "123", + "Value": 123, + "Description": "123" + } + ], + "Namespace": "UnbrandedTypeSpec", + "Description": "The Thing_requiredLiteralInt", + "IsExtensible": true, + "IsNullable": false, + "Usage": "RoundTrip" + }, + { + "$id": "8", + "Kind": "enum", + "Name": "Thing_requiredLiteralFloat", + "ValueType": { + "$id": "9", + "Kind": "float32", + "IsNullable": false + }, + "Values": [ + { + "$id": "10", + "Name": "1.23", + "Value": 1.23, + "Description": "1.23" + } + ], + "Namespace": "UnbrandedTypeSpec", + "Description": "The Thing_requiredLiteralFloat", + "IsExtensible": true, + "IsNullable": false, + "Usage": "RoundTrip" + }, + { + "$id": "11", + "Kind": "enum", + "Name": "Thing_optionalLiteralString", + "ValueType": { + "$id": "12", + "Kind": "string", + "IsNullable": false + }, + "Values": [ + { + "$id": "13", + "Name": "reject", + "Value": "reject", + "Description": "reject" + } + ], + "Namespace": "UnbrandedTypeSpec", + "Description": "The Thing_optionalLiteralString", + "IsExtensible": true, + "IsNullable": false, + "Usage": "RoundTrip" + }, + { + "$id": "14", + "Kind": "enum", + "Name": "Thing_optionalLiteralInt", + "ValueType": { + "$id": "15", + "Kind": "int32", + "IsNullable": false + }, + "Values": [ + { + "$id": "16", + "Name": "456", + "Value": 456, + "Description": "456" + } + ], + "Namespace": "UnbrandedTypeSpec", + "Description": "The Thing_optionalLiteralInt", + "IsExtensible": true, + "IsNullable": false, + "Usage": "RoundTrip" + }, + { + "$id": "17", + "Kind": "enum", + "Name": "Thing_optionalLiteralFloat", + "ValueType": { + "$id": "18", + "Kind": "float32", + "IsNullable": false + }, + "Values": [ + { + "$id": "19", + "Name": "4.56", + "Value": 4.56, + "Description": "4.56" + } + ], + "Namespace": "UnbrandedTypeSpec", + "Description": "The Thing_optionalLiteralFloat", + "IsExtensible": true, + "IsNullable": false, + "Usage": "RoundTrip" + }, + { + "$id": "20", + "Kind": "enum", + "Name": "StringFixedEnum", + "ValueType": { + "$id": "21", + "Kind": "string", + "IsNullable": false + }, + "Values": [ + { + "$id": "22", + "Name": "One", + "Value": "1" + }, + { + "$id": "23", + "Name": "Two", + "Value": "2" + }, + { + "$id": "24", + "Name": "Four", + "Value": "4" + } + ], + "Namespace": "", + "Description": "Simple enum", + "IsExtensible": false, + "IsNullable": true, + "Usage": "RoundTrip" + }, + { + "$id": "25", + "Kind": "enum", + "Name": "StringExtensibleEnum", + "ValueType": { + "$id": "26", + "Kind": "string", + "IsNullable": false + }, + "Values": [ + { + "$id": "27", + "Name": "One", + "Value": "1" + }, + { + "$id": "28", + "Name": "Two", + "Value": "2" + }, + { + "$id": "29", + "Name": "Four", + "Value": "4" + } + ], + "Namespace": "", + "Description": "Extensible enum", + "IsExtensible": true, + "IsNullable": true, + "Usage": "RoundTrip" + }, + { + "$id": "30", + "Kind": "enum", + "Name": "IntExtensibleEnum", + "ValueType": { + "$id": "31", + "Kind": "int32", + "IsNullable": false + }, + "Values": [ + { + "$id": "32", + "Name": "One", + "Value": 1 + }, + { + "$id": "33", + "Name": "Two", + "Value": 2 + }, + { + "$id": "34", + "Name": "Four", + "Value": 4 + } + ], + "Namespace": "UnbrandedTypeSpec", + "Description": "Int based extensible enum", + "IsExtensible": true, + "IsNullable": false, + "Usage": "RoundTrip" + }, + { + "$id": "35", + "Kind": "enum", + "Name": "FloatExtensibleEnum", + "ValueType": { + "$id": "36", + "Kind": "float32", + "IsNullable": false + }, + "Values": [ + { + "$id": "37", + "Name": "OneDotOne", + "Value": 1.1 + }, + { + "$id": "38", + "Name": "TwoDotTwo", + "Value": 2.2 + }, + { + "$id": "39", + "Name": "FourDotFour", + "Value": 4.4 + } + ], + "Namespace": "UnbrandedTypeSpec", + "Description": "Float based extensible enum", + "IsExtensible": true, + "IsNullable": false, + "Usage": "RoundTrip" + }, + { + "$id": "40", + "Kind": "enum", + "Name": "FloatExtensibleEnumWithIntValue", + "ValueType": { + "$id": "41", + "Kind": "float32", + "IsNullable": false + }, + "Values": [ + { + "$id": "42", + "Name": "One", + "Value": 1 + }, + { + "$id": "43", + "Name": "Two", + "Value": 2 + }, + { + "$id": "44", + "Name": "Four", + "Value": 4 + } + ], + "Namespace": "UnbrandedTypeSpec", + "Description": "float fixed enum", + "IsExtensible": true, + "IsNullable": false, + "Usage": "RoundTrip" + }, + { + "$id": "45", + "Kind": "enum", + "Name": "FloatFixedEnum", + "ValueType": { + "$id": "46", + "Kind": "float32", + "IsNullable": false + }, + "Values": [ + { + "$id": "47", + "Name": "OneDotOne", + "Value": 1.1 + }, + { + "$id": "48", + "Name": "TwoDotTwo", + "Value": 2.2 + }, + { + "$id": "49", + "Name": "FourDotFour", + "Value": 4.4 + } + ], + "Namespace": "UnbrandedTypeSpec", + "Description": "float fixed enum", + "IsExtensible": false, + "IsNullable": false, + "Usage": "RoundTrip" + }, + { + "$id": "50", + "Kind": "enum", + "Name": "FloatFixedEnumWithIntValue", + "ValueType": { + "$id": "51", + "Kind": "int32", + "IsNullable": false + }, + "Values": [ + { + "$id": "52", + "Name": "One", + "Value": 1 + }, + { + "$id": "53", + "Name": "Two", + "Value": 2 + }, + { + "$id": "54", + "Name": "Four", + "Value": 4 + } + ], + "Namespace": "UnbrandedTypeSpec", + "Description": "float fixed enum", + "IsExtensible": false, + "IsNullable": false, + "Usage": "RoundTrip" + }, + { + "$id": "55", + "Kind": "enum", + "Name": "IntFixedEnum", + "ValueType": { + "$id": "56", + "Kind": "int32", + "IsNullable": false + }, + "Values": [ + { + "$id": "57", + "Name": "One", + "Value": 1 + }, + { + "$id": "58", + "Name": "Two", + "Value": 2 + }, + { + "$id": "59", + "Name": "Four", + "Value": 4 + } + ], + "Namespace": "UnbrandedTypeSpec", + "Description": "int fixed enum", + "IsExtensible": false, + "IsNullable": false, + "Usage": "RoundTrip" + } + ], + "Models": [ + { + "$id": "60", + "Kind": "Model", + "Name": "Thing", + "Namespace": "UnbrandedTypeSpec", + "Description": "A model with a few properties of literal types", + "IsNullable": false, + "Usage": "RoundTrip", + "Properties": [ + { + "$id": "61", + "Name": "name", + "SerializedName": "name", + "Description": "name of the Thing", + "Type": { + "$id": "62", + "Kind": "string", + "IsNullable": false + }, + "IsRequired": true, + "IsReadOnly": false + }, + { + "$id": "63", + "Name": "requiredUnion", + "SerializedName": "requiredUnion", + "Description": "required Union", + "Type": { + "$id": "64", + "Kind": "Union", + "Name": "Union", + "UnionItemTypes": [ + { + "$id": "65", + "Kind": "string", + "IsNullable": false + }, + { + "$id": "66", + "Kind": "Array", + "Name": "Array", + "ElementType": { + "$id": "67", + "Kind": "string", + "IsNullable": false + }, + "IsNullable": false + }, + { + "$id": "68", + "Kind": "int32", + "IsNullable": false + } + ], + "IsNullable": false + }, + "IsRequired": true, + "IsReadOnly": false + }, + { + "$id": "69", + "Name": "requiredLiteralString", + "SerializedName": "requiredLiteralString", + "Description": "required literal string", + "Type": { + "$id": "70", + "Kind": "constant", + "ValueType": { + "$ref": "2" + }, + "Value": "accept", + "IsNullable": false + }, + "IsRequired": true, + "IsReadOnly": false + }, + { + "$id": "71", + "Name": "requiredLiteralInt", + "SerializedName": "requiredLiteralInt", + "Description": "required literal int", + "Type": { + "$id": "72", + "Kind": "constant", + "ValueType": { + "$ref": "5" + }, + "Value": 123, + "IsNullable": false + }, + "IsRequired": true, + "IsReadOnly": false + }, + { + "$id": "73", + "Name": "requiredLiteralFloat", + "SerializedName": "requiredLiteralFloat", + "Description": "required literal float", + "Type": { + "$id": "74", + "Kind": "constant", + "ValueType": { + "$ref": "8" + }, + "Value": 1.23, + "IsNullable": false + }, + "IsRequired": true, + "IsReadOnly": false + }, + { + "$id": "75", + "Name": "requiredLiteralBool", + "SerializedName": "requiredLiteralBool", + "Description": "required literal bool", + "Type": { + "$id": "76", + "Kind": "constant", + "ValueType": { + "$id": "77", + "Kind": "boolean", + "IsNullable": false + }, + "Value": false, + "IsNullable": false + }, + "IsRequired": true, + "IsReadOnly": false + }, + { + "$id": "78", + "Name": "optionalLiteralString", + "SerializedName": "optionalLiteralString", + "Description": "optional literal string", + "Type": { + "$id": "79", + "Kind": "constant", + "ValueType": { + "$ref": "11" + }, + "Value": "reject", + "IsNullable": false + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "80", + "Name": "optionalLiteralInt", + "SerializedName": "optionalLiteralInt", + "Description": "optional literal int", + "Type": { + "$id": "81", + "Kind": "constant", + "ValueType": { + "$ref": "14" + }, + "Value": 456, + "IsNullable": false + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "82", + "Name": "optionalLiteralFloat", + "SerializedName": "optionalLiteralFloat", + "Description": "optional literal float", + "Type": { + "$id": "83", + "Kind": "constant", + "ValueType": { + "$ref": "17" + }, + "Value": 4.56, + "IsNullable": false + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "84", + "Name": "optionalLiteralBool", + "SerializedName": "optionalLiteralBool", + "Description": "optional literal bool", + "Type": { + "$id": "85", + "Kind": "constant", + "ValueType": { + "$id": "86", + "Kind": "boolean", + "IsNullable": false + }, + "Value": true, + "IsNullable": false + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "87", + "Name": "requiredBadDescription", + "SerializedName": "requiredBadDescription", + "Description": "description with xml <|endoftext|>", + "Type": { + "$id": "88", + "Kind": "string", + "IsNullable": false + }, + "IsRequired": true, + "IsReadOnly": false + }, + { + "$id": "89", + "Name": "optionalNullableList", + "SerializedName": "optionalNullableList", + "Description": "optional nullable collection", + "Type": { + "$id": "90", + "Kind": "Array", + "Name": "Array", + "ElementType": { + "$id": "91", + "Kind": "int32", + "IsNullable": false + }, + "IsNullable": true + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "92", + "Name": "requiredNullableList", + "SerializedName": "requiredNullableList", + "Description": "required nullable collection", + "Type": { + "$id": "93", + "Kind": "Array", + "Name": "Array", + "ElementType": { + "$id": "94", + "Kind": "int32", + "IsNullable": false + }, + "IsNullable": true + }, + "IsRequired": true, + "IsReadOnly": false + } + ] + }, + { + "$id": "95", + "Kind": "Model", + "Name": "RoundTripModel", + "Namespace": "UnbrandedTypeSpec", + "Description": "this is a roundtrip model", + "IsNullable": false, + "Usage": "RoundTrip", + "Properties": [ + { + "$id": "96", + "Name": "requiredString", + "SerializedName": "requiredString", + "Description": "Required string, illustrating a reference type property.", + "Type": { + "$id": "97", + "Kind": "string", + "IsNullable": false + }, + "IsRequired": true, + "IsReadOnly": false + }, + { + "$id": "98", + "Name": "requiredInt", + "SerializedName": "requiredInt", + "Description": "Required int, illustrating a value type property.", + "Type": { + "$id": "99", + "Kind": "int32", + "IsNullable": false + }, + "IsRequired": true, + "IsReadOnly": false + }, + { + "$id": "100", + "Name": "requiredCollection", + "SerializedName": "requiredCollection", + "Description": "Required collection of enums", + "Type": { + "$id": "101", + "Kind": "Array", + "Name": "Array", + "ElementType": { + "$ref": "20" + }, + "IsNullable": false + }, + "IsRequired": true, + "IsReadOnly": false + }, + { + "$id": "102", + "Name": "requiredDictionary", + "SerializedName": "requiredDictionary", + "Description": "Required dictionary of enums", + "Type": { + "$id": "103", + "Kind": "Dictionary", + "Name": "Dictionary", + "KeyType": { + "$id": "104", + "Kind": "string", + "IsNullable": false + }, + "ValueType": { + "$ref": "25" + }, + "IsNullable": false + }, + "IsRequired": true, + "IsReadOnly": false + }, + { + "$id": "105", + "Name": "requiredModel", + "SerializedName": "requiredModel", + "Description": "Required model", + "Type": { + "$ref": "60" + }, + "IsRequired": true, + "IsReadOnly": false + }, + { + "$id": "106", + "Name": "intExtensibleEnum", + "SerializedName": "intExtensibleEnum", + "Description": "this is an int based extensible enum", + "Type": { + "$ref": "30" + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "107", + "Name": "intExtensibleEnumCollection", + "SerializedName": "intExtensibleEnumCollection", + "Description": "this is a collection of int based extensible enum", + "Type": { + "$id": "108", + "Kind": "Array", + "Name": "Array", + "ElementType": { + "$ref": "30" + }, + "IsNullable": false + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "109", + "Name": "floatExtensibleEnum", + "SerializedName": "floatExtensibleEnum", + "Description": "this is a float based extensible enum", + "Type": { + "$ref": "35" + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "110", + "Name": "floatExtensibleEnumWithIntValue", + "SerializedName": "floatExtensibleEnumWithIntValue", + "Description": "this is a float based extensible enum", + "Type": { + "$ref": "40" + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "111", + "Name": "floatExtensibleEnumCollection", + "SerializedName": "floatExtensibleEnumCollection", + "Description": "this is a collection of float based extensible enum", + "Type": { + "$id": "112", + "Kind": "Array", + "Name": "Array", + "ElementType": { + "$ref": "35" + }, + "IsNullable": false + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "113", + "Name": "floatFixedEnum", + "SerializedName": "floatFixedEnum", + "Description": "this is a float based fixed enum", + "Type": { + "$ref": "45" + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "114", + "Name": "floatFixedEnumWithIntValue", + "SerializedName": "floatFixedEnumWithIntValue", + "Description": "this is a float based fixed enum", + "Type": { + "$ref": "50" + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "115", + "Name": "floatFixedEnumCollection", + "SerializedName": "floatFixedEnumCollection", + "Description": "this is a collection of float based fixed enum", + "Type": { + "$id": "116", + "Kind": "Array", + "Name": "Array", + "ElementType": { + "$ref": "45" + }, + "IsNullable": false + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "117", + "Name": "intFixedEnum", + "SerializedName": "intFixedEnum", + "Description": "this is a int based fixed enum", + "Type": { + "$ref": "55" + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "118", + "Name": "intFixedEnumCollection", + "SerializedName": "intFixedEnumCollection", + "Description": "this is a collection of int based fixed enum", + "Type": { + "$id": "119", + "Kind": "Array", + "Name": "Array", + "ElementType": { + "$ref": "55" + }, + "IsNullable": false + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "120", + "Name": "stringFixedEnum", + "SerializedName": "stringFixedEnum", + "Description": "this is a string based fixed enum", + "Type": { + "$ref": "20" + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "121", + "Name": "requiredUnknown", + "SerializedName": "requiredUnknown", + "Description": "required unknown", + "Type": { + "$id": "122", + "Kind": "any", + "IsNullable": false + }, + "IsRequired": true, + "IsReadOnly": false + }, + { + "$id": "123", + "Name": "optionalUnknown", + "SerializedName": "optionalUnknown", + "Description": "optional unknown", + "Type": { + "$id": "124", + "Kind": "any", + "IsNullable": false + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "125", + "Name": "requiredRecordUnknown", + "SerializedName": "requiredRecordUnknown", + "Description": "required record of unknown", + "Type": { + "$id": "126", + "Kind": "Dictionary", + "Name": "Dictionary", + "KeyType": { + "$id": "127", + "Kind": "string", + "IsNullable": false + }, + "ValueType": { + "$id": "128", + "Kind": "any", + "IsNullable": false + }, + "IsNullable": false + }, + "IsRequired": true, + "IsReadOnly": false + }, + { + "$id": "129", + "Name": "optionalRecordUnknown", + "SerializedName": "optionalRecordUnknown", + "Description": "optional record of unknown", + "Type": { + "$id": "130", + "Kind": "Dictionary", + "Name": "Dictionary", + "KeyType": { + "$id": "131", + "Kind": "string", + "IsNullable": false + }, + "ValueType": { + "$id": "132", + "Kind": "any", + "IsNullable": false + }, + "IsNullable": false + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "133", + "Name": "readOnlyRequiredRecordUnknown", + "SerializedName": "readOnlyRequiredRecordUnknown", + "Description": "required readonly record of unknown", + "Type": { + "$id": "134", + "Kind": "Dictionary", + "Name": "Dictionary", + "KeyType": { + "$id": "135", + "Kind": "string", + "IsNullable": false + }, + "ValueType": { + "$id": "136", + "Kind": "any", + "IsNullable": false + }, + "IsNullable": false + }, + "IsRequired": true, + "IsReadOnly": true + }, + { + "$id": "137", + "Name": "readOnlyOptionalRecordUnknown", + "SerializedName": "readOnlyOptionalRecordUnknown", + "Description": "optional readonly record of unknown", + "Type": { + "$id": "138", + "Kind": "Dictionary", + "Name": "Dictionary", + "KeyType": { + "$id": "139", + "Kind": "string", + "IsNullable": false + }, + "ValueType": { + "$id": "140", + "Kind": "any", + "IsNullable": false + }, + "IsNullable": false + }, + "IsRequired": false, + "IsReadOnly": true + }, + { + "$id": "141", + "Name": "modelWithRequiredNullable", + "SerializedName": "modelWithRequiredNullable", + "Description": "this is a model with required nullable properties", + "Type": { + "$id": "142", + "Kind": "Model", + "Name": "ModelWithRequiredNullableProperties", + "Namespace": "UnbrandedTypeSpec", + "Description": "A model with a few required nullable properties", + "IsNullable": false, + "Usage": "RoundTrip", + "Properties": [ + { + "$id": "143", + "Name": "requiredNullablePrimitive", + "SerializedName": "requiredNullablePrimitive", + "Description": "required nullable primitive type", + "Type": { + "$id": "144", + "Kind": "int32", + "IsNullable": true + }, + "IsRequired": true, + "IsReadOnly": false + }, + { + "$id": "145", + "Name": "requiredExtensibleEnum", + "SerializedName": "requiredExtensibleEnum", + "Description": "required nullable extensible enum type", + "Type": { + "$ref": "25" + }, + "IsRequired": true, + "IsReadOnly": false + }, + { + "$id": "146", + "Name": "requiredFixedEnum", + "SerializedName": "requiredFixedEnum", + "Description": "required nullable fixed enum type", + "Type": { + "$ref": "20" + }, + "IsRequired": true, + "IsReadOnly": false + } + ] + }, + "IsRequired": true, + "IsReadOnly": false + }, + { + "$id": "147", + "Name": "requiredBytes", + "SerializedName": "requiredBytes", + "Description": "Required bytes", + "Type": { + "$id": "148", + "Kind": "bytes", + "IsNullable": false, + "Encode": "base64" + }, + "IsRequired": true, + "IsReadOnly": false + } + ] + }, + { + "$ref": "142" + }, + { + "$id": "149", + "Kind": "Model", + "Name": "Friend", + "Namespace": "UnbrandedTypeSpec", + "Description": "this is not a friendly model but with a friendly name", + "IsNullable": false, + "Usage": "RoundTrip", + "Properties": [ + { + "$id": "150", + "Name": "name", + "SerializedName": "name", + "Description": "name of the NotFriend", + "Type": { + "$id": "151", + "Kind": "string", + "IsNullable": false + }, + "IsRequired": true, + "IsReadOnly": false + } + ] + }, + { + "$id": "152", + "Kind": "Model", + "Name": "ProjectedModel", + "Namespace": "UnbrandedTypeSpec", + "Description": "this is a model with a projected name", + "IsNullable": false, + "Usage": "RoundTrip", + "Properties": [ + { + "$id": "153", + "Name": "name", + "SerializedName": "name", + "Description": "name of the ModelWithProjectedName", + "Type": { + "$id": "154", + "Kind": "string", + "IsNullable": false + }, + "IsRequired": true, + "IsReadOnly": false + } + ] + }, + { + "$id": "155", + "Kind": "Model", + "Name": "ReturnsAnonymousModelResponse", + "Namespace": "UnbrandedTypeSpec", + "IsNullable": false, + "Usage": "Output", + "Properties": [] + } + ], + "Clients": [ + { + "$id": "156", + "Name": "UnbrandedTypeSpecClient", + "Description": "This is a sample typespec project.", + "Operations": [ + { + "$id": "157", + "Name": "sayHi", + "ResourceName": "UnbrandedTypeSpec", + "Description": "Return hi", + "Parameters": [ + { + "$id": "158", + "Name": "unbrandedTypeSpecUrl", + "NameInRequest": "unbrandedTypeSpecUrl", + "Type": { + "$id": "159", + "Kind": "uri", + "IsNullable": false + }, + "Location": "Uri", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": true, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Client" + }, + { + "$id": "160", + "Name": "headParameter", + "NameInRequest": "head-parameter", + "Type": { + "$id": "161", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Method" + }, + { + "$id": "162", + "Name": "queryParameter", + "NameInRequest": "queryParameter", + "Type": { + "$id": "163", + "Kind": "string", + "IsNullable": false + }, + "Location": "Query", + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Method" + }, + { + "$id": "164", + "Name": "optionalQuery", + "NameInRequest": "optionalQuery", + "Type": { + "$id": "165", + "Kind": "string", + "IsNullable": false + }, + "Location": "Query", + "IsRequired": false, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Method" + }, + { + "$id": "166", + "Name": "accept", + "NameInRequest": "Accept", + "Type": { + "$id": "167", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "168", + "Type": { + "$ref": "167" + }, + "Value": "application/json" + } + } + ], + "Responses": [ + { + "$id": "169", + "StatusCodes": [200], + "BodyType": { + "$ref": "60" + }, + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false, + "ContentTypes": ["application/json"] + } + ], + "HttpMethod": "GET", + "RequestBodyMediaType": "None", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/hello", + "BufferResponse": true, + "GenerateProtocolMethod": true, + "GenerateConvenienceMethod": true + }, + { + "$id": "170", + "Name": "helloAgain", + "ResourceName": "UnbrandedTypeSpec", + "Description": "Return hi again", + "Parameters": [ + { + "$ref": "158" + }, + { + "$id": "171", + "Name": "p1", + "NameInRequest": "p1", + "Type": { + "$id": "172", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Method" + }, + { + "$id": "173", + "Name": "contentType", + "NameInRequest": "content-type", + "Type": { + "$id": "174", + "Kind": "constant", + "ValueType": { + "$id": "175", + "Kind": "string", + "IsNullable": false + }, + "Value": "text/plain", + "IsNullable": false + }, + "Location": "Header", + "DefaultValue": { + "$id": "176", + "Type": { + "$ref": "174" + }, + "Value": "text/plain" + }, + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant" + }, + { + "$id": "177", + "Name": "p2", + "NameInRequest": "p2", + "Type": { + "$id": "178", + "Kind": "string", + "IsNullable": false + }, + "Location": "Path", + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Method" + }, + { + "$id": "179", + "Name": "action", + "NameInRequest": "action", + "Type": { + "$ref": "95" + }, + "Location": "Body", + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Method" + }, + { + "$id": "180", + "Name": "accept", + "NameInRequest": "Accept", + "Type": { + "$id": "181", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "182", + "Type": { + "$ref": "181" + }, + "Value": "application/json" + } + } + ], + "Responses": [ + { + "$id": "183", + "StatusCodes": [200], + "BodyType": { + "$ref": "95" + }, + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false, + "ContentTypes": ["application/json"] + } + ], + "HttpMethod": "GET", + "RequestBodyMediaType": "Json", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/againHi/{p2}", + "RequestMediaTypes": ["text/plain"], + "BufferResponse": true, + "GenerateProtocolMethod": true, + "GenerateConvenienceMethod": true + }, + { + "$id": "184", + "Name": "noContentType", + "ResourceName": "UnbrandedTypeSpec", + "Description": "Return hi again", + "Parameters": [ + { + "$ref": "158" + }, + { + "$id": "185", + "Name": "p1", + "NameInRequest": "p1", + "Type": { + "$id": "186", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Method" + }, + { + "$id": "187", + "Name": "p2", + "NameInRequest": "p2", + "Type": { + "$id": "188", + "Kind": "string", + "IsNullable": false + }, + "Location": "Path", + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Method" + }, + { + "$id": "189", + "Name": "action", + "NameInRequest": "action", + "Type": { + "$ref": "95" + }, + "Location": "Body", + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Method" + }, + { + "$id": "190", + "Name": "accept", + "NameInRequest": "Accept", + "Type": { + "$id": "191", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "192", + "Type": { + "$ref": "191" + }, + "Value": "application/json" + } + }, + { + "$id": "193", + "Name": "contentType", + "NameInRequest": "Content-Type", + "Type": { + "$id": "194", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": true, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "195", + "Type": { + "$ref": "194" + }, + "Value": "application/json" + } + } + ], + "Responses": [ + { + "$id": "196", + "StatusCodes": [200], + "BodyType": { + "$ref": "95" + }, + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false, + "ContentTypes": ["application/json"] + } + ], + "HttpMethod": "GET", + "RequestBodyMediaType": "Json", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/noContentType/{p2}", + "RequestMediaTypes": ["application/json"], + "BufferResponse": true, + "GenerateProtocolMethod": true, + "GenerateConvenienceMethod": false + }, + { + "$id": "197", + "Name": "helloDemo2", + "ResourceName": "UnbrandedTypeSpec", + "Description": "Return hi in demo2", + "Parameters": [ + { + "$ref": "158" + }, + { + "$id": "198", + "Name": "accept", + "NameInRequest": "Accept", + "Type": { + "$id": "199", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "200", + "Type": { + "$ref": "199" + }, + "Value": "application/json" + } + } + ], + "Responses": [ + { + "$id": "201", + "StatusCodes": [200], + "BodyType": { + "$ref": "60" + }, + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false, + "ContentTypes": ["application/json"] + } + ], + "HttpMethod": "GET", + "RequestBodyMediaType": "None", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/demoHi", + "BufferResponse": true, + "GenerateProtocolMethod": true, + "GenerateConvenienceMethod": true + }, + { + "$id": "202", + "Name": "createLiteral", + "ResourceName": "UnbrandedTypeSpec", + "Description": "Create with literal value", + "Parameters": [ + { + "$ref": "158" + }, + { + "$id": "203", + "Name": "body", + "NameInRequest": "body", + "Type": { + "$ref": "60" + }, + "Location": "Body", + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Method" + }, + { + "$id": "204", + "Name": "accept", + "NameInRequest": "Accept", + "Type": { + "$id": "205", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "206", + "Type": { + "$ref": "205" + }, + "Value": "application/json" + } + }, + { + "$id": "207", + "Name": "contentType", + "NameInRequest": "Content-Type", + "Type": { + "$id": "208", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": true, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "209", + "Type": { + "$ref": "208" + }, + "Value": "application/json" + } + } + ], + "Responses": [ + { + "$id": "210", + "StatusCodes": [200], + "BodyType": { + "$ref": "60" + }, + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false, + "ContentTypes": ["application/json"] + } + ], + "HttpMethod": "POST", + "RequestBodyMediaType": "Json", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/literal", + "RequestMediaTypes": ["application/json"], + "BufferResponse": true, + "GenerateProtocolMethod": true, + "GenerateConvenienceMethod": true + }, + { + "$id": "211", + "Name": "helloLiteral", + "ResourceName": "UnbrandedTypeSpec", + "Description": "Send literal parameters", + "Parameters": [ + { + "$ref": "158" + }, + { + "$id": "212", + "Name": "p1", + "NameInRequest": "p1", + "Type": { + "$id": "213", + "Kind": "constant", + "ValueType": { + "$id": "214", + "Kind": "string", + "IsNullable": false + }, + "Value": "test", + "IsNullable": false + }, + "Location": "Header", + "DefaultValue": { + "$id": "215", + "Type": { + "$ref": "213" + }, + "Value": "test" + }, + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant" + }, + { + "$id": "216", + "Name": "p2", + "NameInRequest": "p2", + "Type": { + "$id": "217", + "Kind": "constant", + "ValueType": { + "$id": "218", + "Kind": "int32", + "IsNullable": false + }, + "Value": 123, + "IsNullable": false + }, + "Location": "Path", + "DefaultValue": { + "$id": "219", + "Type": { + "$ref": "217" + }, + "Value": 123 + }, + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant" + }, + { + "$id": "220", + "Name": "p3", + "NameInRequest": "p3", + "Type": { + "$id": "221", + "Kind": "constant", + "ValueType": { + "$id": "222", + "Kind": "boolean", + "IsNullable": false + }, + "Value": true, + "IsNullable": false + }, + "Location": "Query", + "DefaultValue": { + "$id": "223", + "Type": { + "$ref": "221" + }, + "Value": true + }, + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant" + }, + { + "$id": "224", + "Name": "accept", + "NameInRequest": "Accept", + "Type": { + "$id": "225", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "226", + "Type": { + "$ref": "225" + }, + "Value": "application/json" + } + } + ], + "Responses": [ + { + "$id": "227", + "StatusCodes": [200], + "BodyType": { + "$ref": "60" + }, + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false, + "ContentTypes": ["application/json"] + } + ], + "HttpMethod": "GET", + "RequestBodyMediaType": "None", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/helloLiteral/{p2}", + "BufferResponse": true, + "GenerateProtocolMethod": true, + "GenerateConvenienceMethod": true + }, + { + "$id": "228", + "Name": "topAction", + "ResourceName": "UnbrandedTypeSpec", + "Description": "top level method", + "Parameters": [ + { + "$ref": "158" + }, + { + "$id": "229", + "Name": "action", + "NameInRequest": "action", + "Type": { + "$id": "230", + "Kind": "utcDateTime", + "IsNullable": false, + "Encode": "rfc3339", + "WireType": { + "$id": "231", + "Kind": "string", + "IsNullable": false + } + }, + "Location": "Path", + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Method" + }, + { + "$id": "232", + "Name": "accept", + "NameInRequest": "Accept", + "Type": { + "$id": "233", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "234", + "Type": { + "$ref": "233" + }, + "Value": "application/json" + } + } + ], + "Responses": [ + { + "$id": "235", + "StatusCodes": [200], + "BodyType": { + "$ref": "60" + }, + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false, + "ContentTypes": ["application/json"] + } + ], + "HttpMethod": "GET", + "RequestBodyMediaType": "None", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/top/{action}", + "BufferResponse": true, + "GenerateProtocolMethod": true, + "GenerateConvenienceMethod": true + }, + { + "$id": "236", + "Name": "topAction2", + "ResourceName": "UnbrandedTypeSpec", + "Description": "top level method2", + "Parameters": [ + { + "$ref": "158" + }, + { + "$id": "237", + "Name": "accept", + "NameInRequest": "Accept", + "Type": { + "$id": "238", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "239", + "Type": { + "$ref": "238" + }, + "Value": "application/json" + } + } + ], + "Responses": [ + { + "$id": "240", + "StatusCodes": [200], + "BodyType": { + "$ref": "60" + }, + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false, + "ContentTypes": ["application/json"] + } + ], + "HttpMethod": "GET", + "RequestBodyMediaType": "None", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/top2", + "BufferResponse": true, + "GenerateProtocolMethod": true, + "GenerateConvenienceMethod": false + }, + { + "$id": "241", + "Name": "patchAction", + "ResourceName": "UnbrandedTypeSpec", + "Description": "top level patch", + "Parameters": [ + { + "$ref": "158" + }, + { + "$id": "242", + "Name": "body", + "NameInRequest": "body", + "Type": { + "$ref": "60" + }, + "Location": "Body", + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Method" + }, + { + "$id": "243", + "Name": "accept", + "NameInRequest": "Accept", + "Type": { + "$id": "244", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "245", + "Type": { + "$ref": "244" + }, + "Value": "application/json" + } + }, + { + "$id": "246", + "Name": "contentType", + "NameInRequest": "Content-Type", + "Type": { + "$id": "247", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": true, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "248", + "Type": { + "$ref": "247" + }, + "Value": "application/json" + } + } + ], + "Responses": [ + { + "$id": "249", + "StatusCodes": [200], + "BodyType": { + "$ref": "60" + }, + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false, + "ContentTypes": ["application/json"] + } + ], + "HttpMethod": "PATCH", + "RequestBodyMediaType": "Json", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/patch", + "RequestMediaTypes": ["application/json"], + "BufferResponse": true, + "GenerateProtocolMethod": true, + "GenerateConvenienceMethod": false + }, + { + "$id": "250", + "Name": "anonymousBody", + "ResourceName": "UnbrandedTypeSpec", + "Description": "body parameter without body decorator", + "Parameters": [ + { + "$ref": "158" + }, + { + "$id": "251", + "Name": "Thing", + "NameInRequest": "Thing", + "Description": "A model with a few properties of literal types", + "Type": { + "$ref": "60" + }, + "Location": "Body", + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Method" + }, + { + "$id": "252", + "Name": "accept", + "NameInRequest": "Accept", + "Type": { + "$id": "253", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "254", + "Type": { + "$ref": "253" + }, + "Value": "application/json" + } + }, + { + "$id": "255", + "Name": "contentType", + "NameInRequest": "Content-Type", + "Type": { + "$id": "256", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": true, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "257", + "Type": { + "$ref": "256" + }, + "Value": "application/json" + } + } + ], + "Responses": [ + { + "$id": "258", + "StatusCodes": [200], + "BodyType": { + "$ref": "60" + }, + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false, + "ContentTypes": ["application/json"] + } + ], + "HttpMethod": "POST", + "RequestBodyMediaType": "Json", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/anonymousBody", + "RequestMediaTypes": ["application/json"], + "BufferResponse": true, + "GenerateProtocolMethod": true, + "GenerateConvenienceMethod": true + }, + { + "$id": "259", + "Name": "friendlyModel", + "ResourceName": "UnbrandedTypeSpec", + "Description": "Model can have its friendly name", + "Parameters": [ + { + "$ref": "158" + }, + { + "$id": "260", + "Name": "Friend", + "NameInRequest": "NotFriend", + "Description": "this is not a friendly model but with a friendly name", + "Type": { + "$ref": "149" + }, + "Location": "Body", + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Method" + }, + { + "$id": "261", + "Name": "accept", + "NameInRequest": "Accept", + "Type": { + "$id": "262", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "263", + "Type": { + "$ref": "262" + }, + "Value": "application/json" + } + }, + { + "$id": "264", + "Name": "contentType", + "NameInRequest": "Content-Type", + "Type": { + "$id": "265", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": true, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "266", + "Type": { + "$ref": "265" + }, + "Value": "application/json" + } + } + ], + "Responses": [ + { + "$id": "267", + "StatusCodes": [200], + "BodyType": { + "$ref": "149" + }, + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false, + "ContentTypes": ["application/json"] + } + ], + "HttpMethod": "POST", + "RequestBodyMediaType": "Json", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/friendlyName", + "RequestMediaTypes": ["application/json"], + "BufferResponse": true, + "GenerateProtocolMethod": true, + "GenerateConvenienceMethod": true + }, + { + "$id": "268", + "Name": "addTimeHeader", + "ResourceName": "UnbrandedTypeSpec", + "Parameters": [ + { + "$ref": "158" + }, + { + "$id": "269", + "Name": "repeatabilityFirstSent", + "NameInRequest": "Repeatability-First-Sent", + "Type": { + "$id": "270", + "Kind": "utcDateTime", + "IsNullable": false, + "Encode": "rfc7231", + "WireType": { + "$id": "271", + "Kind": "string", + "IsNullable": false + } + }, + "Location": "Header", + "IsRequired": false, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Method" + }, + { + "$id": "272", + "Name": "accept", + "NameInRequest": "Accept", + "Type": { + "$id": "273", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "274", + "Type": { + "$ref": "273" + }, + "Value": "application/json" + } + } + ], + "Responses": [ + { + "$id": "275", + "StatusCodes": [204], + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false + } + ], + "HttpMethod": "GET", + "RequestBodyMediaType": "None", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/", + "BufferResponse": true, + "GenerateProtocolMethod": true, + "GenerateConvenienceMethod": true + }, + { + "$id": "276", + "Name": "projectedNameModel", + "ResourceName": "UnbrandedTypeSpec", + "Description": "Model can have its projected name", + "Parameters": [ + { + "$ref": "158" + }, + { + "$id": "277", + "Name": "ProjectedModel", + "NameInRequest": "ModelWithProjectedName", + "Description": "this is a model with a projected name", + "Type": { + "$ref": "152" + }, + "Location": "Body", + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Method" + }, + { + "$id": "278", + "Name": "accept", + "NameInRequest": "Accept", + "Type": { + "$id": "279", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "280", + "Type": { + "$ref": "279" + }, + "Value": "application/json" + } + }, + { + "$id": "281", + "Name": "contentType", + "NameInRequest": "Content-Type", + "Type": { + "$id": "282", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": true, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "283", + "Type": { + "$ref": "282" + }, + "Value": "application/json" + } + } + ], + "Responses": [ + { + "$id": "284", + "StatusCodes": [200], + "BodyType": { + "$ref": "152" + }, + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false, + "ContentTypes": ["application/json"] + } + ], + "HttpMethod": "POST", + "RequestBodyMediaType": "Json", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/projectedName", + "RequestMediaTypes": ["application/json"], + "BufferResponse": true, + "GenerateProtocolMethod": true, + "GenerateConvenienceMethod": true + }, + { + "$id": "285", + "Name": "returnsAnonymousModel", + "ResourceName": "UnbrandedTypeSpec", + "Description": "return anonymous model", + "Parameters": [ + { + "$ref": "158" + }, + { + "$id": "286", + "Name": "accept", + "NameInRequest": "Accept", + "Type": { + "$id": "287", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "288", + "Type": { + "$ref": "287" + }, + "Value": "application/json" + } + } + ], + "Responses": [ + { + "$id": "289", + "StatusCodes": [200], + "BodyType": { + "$ref": "155" + }, + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false, + "ContentTypes": ["application/json"] + } + ], + "HttpMethod": "POST", + "RequestBodyMediaType": "None", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/returnsAnonymousModel", + "BufferResponse": true, + "GenerateProtocolMethod": true, + "GenerateConvenienceMethod": true + }, + { + "$id": "290", + "Name": "getUnknownValue", + "ResourceName": "UnbrandedTypeSpec", + "Description": "get extensible enum", + "Parameters": [ + { + "$ref": "158" + }, + { + "$id": "291", + "Name": "accept", + "NameInRequest": "Accept", + "Type": { + "$id": "292", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "293", + "Type": { + "$ref": "292" + }, + "Value": "application/json" + } + } + ], + "Responses": [ + { + "$id": "294", + "StatusCodes": [200], + "BodyType": { + "$id": "295", + "Kind": "string", + "IsNullable": false + }, + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false, + "ContentTypes": ["application/json"] + } + ], + "HttpMethod": "GET", + "RequestBodyMediaType": "None", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/unknown-value", + "BufferResponse": true, + "GenerateProtocolMethod": true, + "GenerateConvenienceMethod": true + }, + { + "$id": "296", + "Name": "internalProtocol", + "ResourceName": "UnbrandedTypeSpec", + "Description": "When set protocol false and convenient true, then the protocol method should be internal", + "Parameters": [ + { + "$ref": "158" + }, + { + "$id": "297", + "Name": "body", + "NameInRequest": "body", + "Type": { + "$ref": "60" + }, + "Location": "Body", + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Method" + }, + { + "$id": "298", + "Name": "accept", + "NameInRequest": "Accept", + "Type": { + "$id": "299", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "300", + "Type": { + "$ref": "299" + }, + "Value": "application/json" + } + }, + { + "$id": "301", + "Name": "contentType", + "NameInRequest": "Content-Type", + "Type": { + "$id": "302", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": true, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "303", + "Type": { + "$ref": "302" + }, + "Value": "application/json" + } + } + ], + "Responses": [ + { + "$id": "304", + "StatusCodes": [200], + "BodyType": { + "$ref": "60" + }, + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false, + "ContentTypes": ["application/json"] + } + ], + "HttpMethod": "POST", + "RequestBodyMediaType": "Json", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/internalProtocol", + "RequestMediaTypes": ["application/json"], + "BufferResponse": true, + "GenerateProtocolMethod": false, + "GenerateConvenienceMethod": true + }, + { + "$id": "305", + "Name": "stillConvenient", + "ResourceName": "UnbrandedTypeSpec", + "Description": "When set protocol false and convenient true, the convenient method should be generated even it has the same signature as protocol one", + "Parameters": [ + { + "$ref": "158" + }, + { + "$id": "306", + "Name": "accept", + "NameInRequest": "Accept", + "Type": { + "$id": "307", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "308", + "Type": { + "$ref": "307" + }, + "Value": "application/json" + } + } + ], + "Responses": [ + { + "$id": "309", + "StatusCodes": [204], + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false + } + ], + "HttpMethod": "GET", + "RequestBodyMediaType": "None", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/stillConvenient", + "BufferResponse": true, + "GenerateProtocolMethod": false, + "GenerateConvenienceMethod": true + }, + { + "$id": "310", + "Name": "headAsBoolean", + "ResourceName": "UnbrandedTypeSpec", + "Description": "head as boolean.", + "Parameters": [ + { + "$ref": "158" + }, + { + "$id": "311", + "Name": "id", + "NameInRequest": "id", + "Type": { + "$id": "312", + "Kind": "string", + "IsNullable": false + }, + "Location": "Path", + "IsRequired": true, + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Method" + }, + { + "$id": "313", + "Name": "accept", + "NameInRequest": "Accept", + "Type": { + "$id": "314", + "Kind": "string", + "IsNullable": false + }, + "Location": "Header", + "IsApiVersion": false, + "IsResourceParameter": false, + "IsContentType": false, + "IsRequired": true, + "IsEndpoint": false, + "SkipUrlEncoding": false, + "Explode": false, + "Kind": "Constant", + "DefaultValue": { + "$id": "315", + "Type": { + "$ref": "314" + }, + "Value": "application/json" + } + } + ], + "Responses": [ + { + "$id": "316", + "StatusCodes": [204], + "BodyMediaType": "Json", + "Headers": [], + "IsErrorResponse": false + } + ], + "HttpMethod": "HEAD", + "RequestBodyMediaType": "None", + "Uri": "{unbrandedTypeSpecUrl}", + "Path": "/headAsBoolean/{id}", + "BufferResponse": true, + "GenerateProtocolMethod": true, + "GenerateConvenienceMethod": true + } + ], + "Protocol": { + "$id": "317" + }, + "Creatable": true, + "Parameters": [ + { + "$ref": "158" + } + ] + } + ], + "Auth": { + "$id": "318", + "ApiKey": { + "$id": "319", + "Name": "my-api-key" + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CSharpGen.cs index 95cd95c6f1a..6f4da235643 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CSharpGen.cs @@ -6,10 +6,11 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using Microsoft.Generator.CSharp.Providers; namespace Microsoft.Generator.CSharp { - public sealed class CSharpGen + internal sealed class CSharpGen { private const string ConfigurationFileName = "Configuration.json"; private const string CodeModelFileName = "tspCodeModel.json"; @@ -33,25 +34,35 @@ public async Task ExecuteAsync() foreach (var model in output.Models) { - CodeWriter writer = new CodeWriter(); - CodeModelPlugin.Instance.GetWriter(writer, model).Write(); - generateFilesTasks.Add(workspace.AddGeneratedFile(Path.Combine("src", "Generated", "Models", $"{model.Name}.cs"), writer.ToString())); + generateFilesTasks.Add(workspace.AddGeneratedFile(CodeModelPlugin.Instance.GetWriter(model).Write())); foreach (var serialization in model.SerializationProviders) { - CodeWriter serializationWriter = new CodeWriter(); - CodeModelPlugin.Instance.GetWriter(serializationWriter, serialization).Write(); - generateFilesTasks.Add(workspace.AddGeneratedFile(Path.Combine("src", "Generated", "Models", $"{serialization.Name}.Serialization.cs"), serializationWriter.ToString())); + generateFilesTasks.Add(workspace.AddGeneratedFile(CodeModelPlugin.Instance.GetWriter(serialization).Write())); + } + } + + foreach (var enumType in output.Enums) + { + generateFilesTasks.Add(workspace.AddGeneratedFile(CodeModelPlugin.Instance.GetWriter(enumType).Write())); + + if (enumType.Serialization is { } serialization) + { + generateFilesTasks.Add(workspace.AddGeneratedFile(CodeModelPlugin.Instance.GetWriter(serialization).Write())); } } foreach (var client in output.Clients) { - CodeWriter writer = new CodeWriter(); - CodeModelPlugin.Instance.GetWriter(writer, client).Write(); - generateFilesTasks.Add(workspace.AddGeneratedFile(Path.Combine("src", "Generated", $"{client.Name}.cs"), writer.ToString())); + generateFilesTasks.Add(workspace.AddGeneratedFile(CodeModelPlugin.Instance.GetWriter(client).Write())); } + Directory.CreateDirectory(Path.Combine(outputPath, "src", "Generated", "Internal")); + generateFilesTasks.Add(workspace.AddGeneratedFile(CodeModelPlugin.Instance.GetWriter(ChangeTrackingListProvider.Instance).Write())); + generateFilesTasks.Add(workspace.AddGeneratedFile(CodeModelPlugin.Instance.GetWriter(ChangeTrackingDictionaryProvider.Instance).Write())); + generateFilesTasks.Add(workspace.AddGeneratedFile(CodeModelPlugin.Instance.GetWriter(ArgumentProvider.Instance).Write())); + generateFilesTasks.Add(workspace.AddGeneratedFile(CodeModelPlugin.Instance.GetWriter(OptionalProvider.Instance).Write())); + // Add all the generated files to the workspace await Task.WhenAll(generateFilesTasks); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CodeModelPlugin.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CodeModelPlugin.cs index b9fa186b256..9c840c18601 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CodeModelPlugin.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CodeModelPlugin.cs @@ -4,8 +4,9 @@ using System; using System.Collections.Generic; using System.ComponentModel.Composition; -using Microsoft.Generator.CSharp.Expressions; using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Snippets; namespace Microsoft.Generator.CSharp { @@ -32,17 +33,18 @@ public CodeModelPlugin(GeneratorContext context) // Extensibility points to be implemented by a plugin public abstract ApiTypes ApiTypes { get; } - public abstract CodeWriterExtensionMethods CodeWriterExtensionMethods { get; } public abstract TypeFactory TypeFactory { get; } + public virtual string LicenseString => string.Empty; public abstract ExtensibleSnippets ExtensibleSnippets { get; } public abstract OutputLibrary OutputLibrary { get; } public InputLibrary InputLibrary => _inputLibrary.Value; - public virtual TypeProviderWriter GetWriter(CodeWriter writer, TypeProvider provider) => new(writer, provider); + public virtual TypeProviderWriter GetWriter(TypeProvider provider) => new(provider); /// - /// Returns a serialization type provider for the given model type provider. + /// Returns the serialization type providers for the given model type provider. /// /// The model type provider. - public abstract IReadOnlyList GetSerializationTypeProviders(ModelTypeProvider provider); + /// The input model. + public abstract IReadOnlyList GetSerializationTypeProviders(ModelProvider provider, InputModelType inputModel); } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ArrayElementExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ArrayElementExpression.cs index ff46fcc72ba..22e1c3e4099 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ArrayElementExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ArrayElementExpression.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Generator.CSharp.Expressions @@ -10,7 +10,7 @@ namespace Microsoft.Generator.CSharp.Expressions /// The index of the element in the array. public sealed record ArrayElementExpression(ValueExpression Array, ValueExpression Index) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { Array.Write(writer); writer.AppendRaw("["); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ArrayInitializerExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ArrayInitializerExpression.cs index 2c7909ad7e3..d1b5407a8fb 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ArrayInitializerExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ArrayInitializerExpression.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections.Generic; @@ -10,9 +10,9 @@ namespace Microsoft.Generator.CSharp.Expressions /// /// The elements to initialize the array to. /// Flag to determine if the array should be initialized inline. - public sealed record ArrayInitializerExpression(IReadOnlyList? Elements = null, bool IsInline = true) : InitializerExpression + public sealed record ArrayInitializerExpression(IReadOnlyList? Elements = null, bool IsInline = true) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { if (Elements is not { Count: > 0 }) { @@ -23,26 +23,24 @@ public override void Write(CodeWriter writer) if (IsInline) { writer.AppendRaw("{"); - foreach (var item in Elements) + for (int i = 0; i < Elements.Count; i++) { - item.Write(writer); - writer.AppendRaw(", "); + Elements[i].Write(writer); + if (i < Elements.Count - 1) + writer.AppendRaw(", "); } - - writer.RemoveTrailingComma(); writer.AppendRaw("}"); } else { writer.WriteLine(); writer.WriteRawLine("{"); - foreach (var item in Elements) + for (int i = 0; i < Elements.Count; i++) { - item.Write(writer); - writer.WriteRawLine(","); + Elements[i].Write(writer); + if (i < Elements.Count - 1) + writer.WriteRawLine(","); } - - writer.RemoveTrailingComma(); writer.WriteLine(); writer.AppendRaw("}"); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/AsExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/AsExpression.cs index 3b369ccdee1..0f53136dcc9 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/AsExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/AsExpression.cs @@ -1,11 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Generator.CSharp.Expressions { public sealed record AsExpression(ValueExpression Inner, CSharpType Type) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { Inner.Write(writer); writer.Append($" as {Type}"); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/AssignmentExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/AssignmentExpression.cs index 20e035e9a29..cdab6c8dba8 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/AssignmentExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/AssignmentExpression.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Generator.CSharp.Expressions @@ -8,11 +8,13 @@ namespace Microsoft.Generator.CSharp.Expressions /// /// The variable that is being assigned. /// The value that is being assigned. - public sealed record AssignmentExpression(VariableReference Variable, ValueExpression Value) : ValueExpression + public sealed record AssignmentExpression(ValueExpression Variable, ValueExpression Value) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { - writer.Append($"{Variable.Type} {Variable.Declaration:D} = {Value}"); + Variable.Write(writer); + writer.Append($" = "); + Value.Write(writer); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/BinaryDataExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/BinaryDataExpression.cs deleted file mode 100644 index dabfbf1de7c..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/BinaryDataExpression.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record BinaryDataExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) - { - public FrameworkTypeExpression ToObjectFromJson(Type responseType) - => new(responseType, new InvokeInstanceMethodExpression(Untyped, nameof(BinaryData.ToObjectFromJson), Array.Empty(), new[] { new CSharpType(responseType) }, false)); - - public static BinaryDataExpression FromStream(StreamExpression stream, bool async) - { - var methodName = async ? nameof(BinaryData.FromStreamAsync) : nameof(BinaryData.FromStream); - return new BinaryDataExpression(InvokeStatic(methodName, stream, async)); - } - - public static BinaryDataExpression FromStream(ValueExpression stream, bool async) - { - var methodName = async ? nameof(BinaryData.FromStreamAsync) : nameof(BinaryData.FromStream); - return new(InvokeStatic(methodName, stream, async)); - } - - public ValueExpression ToMemory() => Invoke(nameof(BinaryData.ToMemory)); - - public StreamExpression ToStream() => new(Invoke(nameof(BinaryData.ToStream))); - - public ListExpression ToArray() => new(typeof(byte[]), Invoke(nameof(BinaryData.ToArray))); - - public static BinaryDataExpression FromBytes(ValueExpression data) - => new(InvokeStatic(nameof(BinaryData.FromBytes), data)); - - public static BinaryDataExpression FromObjectAsJson(ValueExpression data) - => new(InvokeStatic(nameof(BinaryData.FromObjectAsJson), data)); - - public static BinaryDataExpression FromString(ValueExpression data) - => new(InvokeStatic(nameof(BinaryData.FromString), data)); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/BinaryOperatorExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/BinaryOperatorExpression.cs index 7063c62e606..4fa063d56fa 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/BinaryOperatorExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/BinaryOperatorExpression.cs @@ -1,11 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Generator.CSharp.Expressions { public sealed record BinaryOperatorExpression(string Operator, ValueExpression Left, ValueExpression Right) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { writer.AppendRaw("("); Left.Write(writer); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/BoolExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/BoolExpression.cs deleted file mode 100644 index 01201e03007..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/BoolExpression.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record BoolExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) - { - public BoolExpression Or(ValueExpression other) => new(new BinaryOperatorExpression(" || ", this, other)); - - public BoolExpression And(ValueExpression other) => new(new BinaryOperatorExpression(" && ", this, other)); - - public static BoolExpression True { get; } = Snippets.True; - - public static BoolExpression False { get; } = Snippets.False; - - public static BoolExpression Is(ValueExpression untyped, CSharpType comparisonType) => new(new BinaryOperatorExpression("is", untyped, comparisonType)); - - public static BoolExpression Is(ValueExpression untyped, DeclarationExpression declaration) => new(new BinaryOperatorExpression("is", untyped, declaration)); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/CancellationTokenExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/CancellationTokenExpression.cs deleted file mode 100644 index 717c1f8ea96..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/CancellationTokenExpression.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Threading; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record CancellationTokenExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) - { - public BoolExpression CanBeCanceled => new(Property(nameof(CancellationToken.CanBeCanceled))); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/CastExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/CastExpression.cs index f84e4e5cead..04011c0b741 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/CastExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/CastExpression.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Generator.CSharp.Expressions @@ -10,7 +10,7 @@ namespace Microsoft.Generator.CSharp.Expressions /// The type to cast to. public sealed record CastExpression(ValueExpression Inner, CSharpType Type) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { // wrap the cast expression with parenthesis, so that it would not cause ambiguity for leading recursive calls // if the parenthesis are not needed, the roslyn reducer will remove it. diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/CatchStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/CatchExpression.cs similarity index 62% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/CatchStatement.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/CatchExpression.cs index a58b3a1276d..bd84fb88435 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/CatchStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/CatchExpression.cs @@ -1,11 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using Microsoft.Generator.CSharp.Statements; + namespace Microsoft.Generator.CSharp.Expressions { - public sealed record CatchStatement(ValueExpression? Exception, MethodBodyStatement Body) + public sealed record CatchExpression(ValueExpression? Exception, MethodBodyStatement Body) : ValueExpression { - public void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { writer.AppendRaw("catch"); if (Exception != null) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/CharExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/CharExpression.cs deleted file mode 100644 index acb53099f90..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/CharExpression.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record CharExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) - { - public StringExpression InvokeToString(ValueExpression cultureInfo) => new(Invoke(nameof(char.ToString), cultureInfo)); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/CollectionInitializerExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/CollectionInitializerExpression.cs index 7bc5c24c9ec..60fef7bbb2f 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/CollectionInitializerExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/CollectionInitializerExpression.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Generator.CSharp.Expressions @@ -9,16 +9,15 @@ namespace Microsoft.Generator.CSharp.Expressions /// The items to set during collection initialization. public sealed record CollectionInitializerExpression(params ValueExpression[] Items) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { writer.AppendRaw("{ "); - foreach (var item in Items) + for (int i = 0; i < Items.Length; i++) { - item.Write(writer); - writer.AppendRaw(","); + Items[i].Write(writer); + if (i < Items.Length - 1) + writer.AppendRaw(", "); } - - writer.RemoveTrailingComma(); writer.AppendRaw(" }"); } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/DateTimeOffsetExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/DateTimeOffsetExpression.cs deleted file mode 100644 index e9488aad0d7..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/DateTimeOffsetExpression.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record DateTimeOffsetExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) - { - public static DateTimeOffsetExpression Now => new(StaticProperty(nameof(DateTimeOffset.Now))); - public static DateTimeOffsetExpression UtcNow => new(StaticProperty(nameof(DateTimeOffset.UtcNow))); - - public static DateTimeOffsetExpression FromUnixTimeSeconds(ValueExpression expression) - => new(InvokeStatic(nameof(DateTimeOffset.FromUnixTimeSeconds), expression)); - - public StringExpression InvokeToString(StringExpression format, ValueExpression formatProvider) - => new(Invoke(nameof(DateTimeOffset.ToString), new[] { format, formatProvider })); - - public LongExpression ToUnixTimeSeconds() - => new(Invoke(nameof(DateTimeOffset.ToUnixTimeSeconds))); - - public DateTimeOffsetExpression ToUniversalTime() - => new(Invoke(nameof(DateTimeOffset.ToUniversalTime))); - - public static DateTimeOffsetExpression Parse(string s) => Parse(Snippets.Literal(s)); - - public static DateTimeOffsetExpression Parse(ValueExpression value) - => new(InvokeStatic(nameof(DateTimeOffset.Parse), value)); - - public static DateTimeOffsetExpression Parse(ValueExpression value, ValueExpression formatProvider, ValueExpression style) - => new(InvokeStatic(nameof(DateTimeOffset.Parse), new[] { value, formatProvider, style })); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/DeclarationExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/DeclarationExpression.cs index d8237564787..d4f0d7a3d4b 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/DeclarationExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/DeclarationExpression.cs @@ -1,19 +1,21 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using Microsoft.Generator.CSharp.Snippets; + namespace Microsoft.Generator.CSharp.Expressions { - public sealed record DeclarationExpression(VariableReference Variable, bool IsOut) : ValueExpression + public sealed record DeclarationExpression(CSharpType Type, CodeWriterDeclaration Declaration, bool IsOut = false) : ValueExpression { - public DeclarationExpression(CSharpType type, string name, out VariableReference variable, bool isOut = false) : this(new VariableReference(type, name), isOut) + public DeclarationExpression(CSharpType type, string name, out VariableReferenceSnippet variable, bool isOut = false) : this(type, new CodeWriterDeclaration(name), isOut) { - variable = Variable; + variable = new VariableReferenceSnippet(Type, Declaration); } - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { writer.AppendRawIf("out ", IsOut); - writer.Append($"{Variable.Type} {Variable.Declaration:D}"); + writer.Append($"{Type} {Declaration:D}"); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/DictionaryExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/DictionaryExpression.cs deleted file mode 100644 index f6f6e675d8a..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/DictionaryExpression.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections.Generic; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record DictionaryExpression(CSharpType KeyType, CSharpType ValueType, ValueExpression Untyped) : TypedValueExpression(new CSharpType(typeof(Dictionary<,>), false, KeyType, ValueType), Untyped) - { - public MethodBodyStatement Add(ValueExpression key, ValueExpression value) - => new InvokeInstanceMethodStatement(Untyped, nameof(Dictionary.Add), key, value); - - public MethodBodyStatement Add(KeyValuePairExpression pair) - => new InvokeInstanceMethodStatement(Untyped, nameof(Dictionary.Add), pair); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/DictionaryInitializerExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/DictionaryInitializerExpression.cs index 746694e6dd8..bdd13c8656c 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/DictionaryInitializerExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/DictionaryInitializerExpression.cs @@ -1,13 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections.Generic; namespace Microsoft.Generator.CSharp.Expressions { - public sealed record DictionaryInitializerExpression(IReadOnlyList<(ValueExpression Key, ValueExpression Value)>? Values = null) : InitializerExpression + public sealed record DictionaryInitializerExpression(IReadOnlyList<(ValueExpression Key, ValueExpression Value)>? Values = null) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { if (Values is not { Count: > 0 }) { @@ -17,16 +17,15 @@ public override void Write(CodeWriter writer) writer.WriteLine(); writer.WriteRawLine("{"); - foreach (var (key, value) in Values) + for (int i = 0; i < Values.Count; i++) { - writer.AppendRaw("["); + var (key, value) = Values[i]; key.Write(writer); - writer.AppendRaw("] = "); + writer.AppendRaw(" = "); value.Write(writer); - writer.WriteRawLine(","); + if (i < Values.Count - 1) + writer.WriteRawLine(","); } - - writer.RemoveTrailingComma(); writer.WriteLine(); writer.AppendRaw("}"); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/DoubleExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/DoubleExpression.cs deleted file mode 100644 index c0d3b50bb0c..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/DoubleExpression.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record DoubleExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) - { - public static DoubleExpression MaxValue => new(StaticProperty(nameof(double.MaxValue))); - - public static BoolExpression IsNan(ValueExpression d) => new(new InvokeStaticMethodExpression(typeof(double), nameof(double.IsNaN), new[] { d })); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/EnumExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/EnumExpression.cs deleted file mode 100644 index b815f5b26bc..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/EnumExpression.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record EnumExpression(EnumTypeProvider EnumType, ValueExpression Untyped) : TypedValueExpression(EnumType.Type, Untyped) - { - public TypedValueExpression ToSerial() - => EnumType.SerializationMethodName is {} name - ? EnumType.IsExtensible - ? new FrameworkTypeExpression(EnumType.ValueType.FrameworkType, Untyped.Invoke(name)) - : new FrameworkTypeExpression(EnumType.ValueType.FrameworkType, new InvokeStaticMethodExpression(EnumType.Type, name, new[] { Untyped }, null, true)) - : EnumType is { IsExtensible: true, IsStringValueType: true } - ? Untyped.InvokeToString() - : throw new InvalidOperationException($"No conversion available fom {EnumType.Type.Name}"); - - public static TypedValueExpression ToEnum(EnumTypeProvider enumType, ValueExpression value) - => enumType.IsExtensible - ? new EnumExpression(enumType, Snippets.New.Instance(enumType.Type, value)) - : new EnumExpression(enumType, new InvokeStaticMethodExpression(enumType.Type, $"To{enumType.Name}", new[] { value }, null, true)); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/EnumerableExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/EnumerableExpression.cs deleted file mode 100644 index 09689275250..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/EnumerableExpression.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections.Generic; -using System.Linq; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record EnumerableExpression(CSharpType ItemType, ValueExpression Untyped) : TypedValueExpression(new CSharpType(typeof(IEnumerable<>), false, ItemType), Untyped) - { - public BoolExpression Any() => new(new InvokeStaticMethodExpression(typeof(Enumerable), nameof(Enumerable.Any), new[] { Untyped }, CallAsExtension: true)); - public EnumerableExpression Select(TypedFuncExpression selector) => new(selector.Inner.Type, new InvokeStaticMethodExpression(typeof(Enumerable), nameof(Enumerable.Select), new[] { Untyped, selector }, CallAsExtension: true)); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/EnvironmentExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/EnvironmentExpression.cs deleted file mode 100644 index 875fe67dac3..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/EnvironmentExpression.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record EnvironmentExpression(ValueExpression Untyped) : TypedValueExpression(typeof(Environment), Untyped) - { - public static StringExpression NewLine() => new(new TypeReference(typeof(Environment)).Property(nameof(Environment.NewLine))); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/FormattableStringExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/FormattableStringExpression.cs index 05536ccc95f..fab355473ac 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/FormattableStringExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/FormattableStringExpression.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -23,20 +23,11 @@ public FormattableStringExpression(string format, IReadOnlyList Args = args; } - public FormattableStringExpression(string format, params ValueExpression[] args) : this(format, args as IReadOnlyList) - { - } - - public string Format { get; init; } - public IReadOnlyList Args { get; init; } - public void Deconstruct(out string format, out IReadOnlyList args) - { - format = Format; - args = Args; - } + private string Format { get; init; } + private IReadOnlyList Args { get; init; } - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { writer.AppendRaw("$\""); var argumentCount = 0; @@ -48,7 +39,7 @@ public override void Write(CodeWriter writer) continue; } - var arg = Args[argumentCount]; + var arg = Args![argumentCount]; argumentCount++; // append the argument writer.AppendRaw("{"); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/FormattableStringToExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/FormattableStringToExpression.cs deleted file mode 100644 index af373ddb444..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/FormattableStringToExpression.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record FormattableStringToExpression(FormattableString Value) : ValueExpression - { - public override void Write(CodeWriter writer) - { - writer.Append(Value); - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/FrameworkTypeExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/FrameworkTypeExpression.cs deleted file mode 100644 index 8ce0e51fd2c..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/FrameworkTypeExpression.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.Generator.CSharp.Expressions -{ - /// - /// Represents expression which has a return value of a framework type. - /// - public sealed record FrameworkTypeExpression(Type FrameworkType, ValueExpression Untyped) : TypedValueExpression(FrameworkType, Untyped); -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/FuncExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/FuncExpression.cs index 2b8aff53ab0..8302ccbc2ea 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/FuncExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/FuncExpression.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections.Generic; @@ -7,7 +7,7 @@ namespace Microsoft.Generator.CSharp.Expressions { public sealed record FuncExpression(IReadOnlyList Parameters, ValueExpression Inner) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { using (writer.AmbientScope()) { @@ -26,8 +26,9 @@ public override void Write(CodeWriter writer) else { writer.AppendRaw("("); - foreach (var parameter in Parameters) + for (int i = 0; i < Parameters.Count; i++) { + var parameter = Parameters[i]; if (parameter is not null) { writer.WriteDeclaration(parameter); @@ -36,10 +37,9 @@ public override void Write(CodeWriter writer) { writer.AppendRaw("_"); } - writer.AppendRaw(", "); + if (i < Parameters.Count - 1) + writer.AppendRaw(", "); } - - writer.RemoveTrailingComma(); writer.AppendRaw(")"); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/IndexerExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/IndexerExpression.cs index c8a47af558e..cfd99681cd6 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/IndexerExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/IndexerExpression.cs @@ -1,11 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Generator.CSharp.Expressions { public sealed record IndexerExpression(ValueExpression? Inner, ValueExpression Index) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { if (Inner is not null) { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/InitializerExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/InitializerExpression.cs deleted file mode 100644 index cb18b945509..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/InitializerExpression.cs +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Expressions -{ - public record InitializerExpression : ValueExpression; -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/IntExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/IntExpression.cs deleted file mode 100644 index 433fed93c24..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/IntExpression.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record IntExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) - { - public static IntExpression MaxValue => new(StaticProperty(nameof(int.MaxValue))); - public IntExpression Add(IntExpression value) => Operator("+", value); - public IntExpression Minus(IntExpression value) => Operator("-", value); - public IntExpression Multiply(IntExpression value) => Operator("*", value); - public IntExpression DivideBy(IntExpression value) => Operator("/", value); - - public static IntExpression operator +(IntExpression left, IntExpression right) => left.Add(right); - public static IntExpression operator -(IntExpression left, IntExpression right) => left.Minus(right); - public static IntExpression operator *(IntExpression left, IntExpression right) => left.Multiply(right); - public static IntExpression operator /(IntExpression left, IntExpression right) => left.DivideBy(right); - - private IntExpression Operator(string op, IntExpression other) => new(new BinaryOperatorExpression(op, this, other)); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/InvokeInstanceMethodExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/InvokeInstanceMethodExpression.cs index ac89b636091..02bb84ccc46 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/InvokeInstanceMethodExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/InvokeInstanceMethodExpression.cs @@ -1,13 +1,16 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections.Generic; using System.Linq; +using Microsoft.Generator.CSharp.Statements; namespace Microsoft.Generator.CSharp.Expressions { public sealed record InvokeInstanceMethodExpression(ValueExpression? InstanceReference, string MethodName, IReadOnlyList Arguments, IReadOnlyList? TypeArguments, bool CallAsAsync, bool AddConfigureAwaitFalse = true) : ValueExpression { + public InvokeInstanceMethodExpression(ValueExpression? instanceReference, string methodName, IReadOnlyList arguments) : this(instanceReference, methodName, arguments, null, false) { } + public InvokeInstanceMethodExpression(ValueExpression? instanceReference, MethodSignature signature, IReadOnlyList arguments, bool addConfigureAwaitFalse = true) : this(instanceReference, signature.Name, arguments, signature.GenericArguments, signature.Modifiers.HasFlag(MethodSignatureModifiers.Async), addConfigureAwaitFalse) { } public InvokeInstanceMethodExpression(ValueExpression? instanceReference, MethodSignature signature, bool addConfigureAwaitFalse = true) : this(instanceReference, signature, signature.Parameters.Select(p => (ValueExpression)p).ToArray(), addConfigureAwaitFalse) { } @@ -15,7 +18,7 @@ public InvokeInstanceMethodExpression(ValueExpression? instanceReference, Method internal MethodBodyStatement ToStatement() => new InvokeInstanceMethodStatement(InstanceReference, MethodName, Arguments, CallAsAsync); - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { writer.AppendRawIf("await ", CallAsAsync); if (InstanceReference != null) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/InvokeStaticMethodExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/InvokeStaticMethodExpression.cs index 3c740ea7792..6e7d802a1d3 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/InvokeStaticMethodExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/InvokeStaticMethodExpression.cs @@ -1,13 +1,17 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections.Generic; using System.Linq; +using Microsoft.Generator.CSharp.Statements; namespace Microsoft.Generator.CSharp.Expressions { public sealed record InvokeStaticMethodExpression(CSharpType? MethodType, string MethodName, IReadOnlyList Arguments, IReadOnlyList? TypeArguments = null, bool CallAsExtension = false, bool CallAsAsync = false) : ValueExpression { + public InvokeStaticMethodExpression(CSharpType? methodType, string methodName, ValueExpression arg, IReadOnlyList? typeArguments = null, bool callAsExtension = false, bool callAsAsync = false) + : this(methodType, methodName, [arg], typeArguments, callAsExtension, callAsAsync) { } + public static InvokeStaticMethodExpression Extension(CSharpType? methodType, string methodName, ValueExpression instanceReference) => new(methodType, methodName, new[] { instanceReference }, CallAsExtension: true); public static InvokeStaticMethodExpression Extension(CSharpType? methodType, string methodName, ValueExpression instanceReference, ValueExpression arg) => new(methodType, methodName, new[] { instanceReference, arg }, CallAsExtension: true); public static InvokeStaticMethodExpression Extension(CSharpType? methodType, string methodName, ValueExpression instanceReference, IReadOnlyList arguments) @@ -16,7 +20,7 @@ public static InvokeStaticMethodExpression Extension(CSharpType? methodType, str public MethodBodyStatement ToStatement() => new InvokeStaticMethodStatement(MethodType, MethodName, Arguments, TypeArguments, CallAsExtension, CallAsAsync); - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { if (CallAsExtension) { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/JsonDocumentExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/JsonDocumentExpression.cs deleted file mode 100644 index 51c4e67f0d9..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/JsonDocumentExpression.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Text.Json; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record JsonDocumentExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) - { - public JsonElementExpression RootElement => new(Property(nameof(JsonDocument.RootElement))); - - public static JsonDocumentExpression ParseValue(ValueExpression reader) => new(InvokeStatic(nameof(JsonDocument.ParseValue), reader)); - public static JsonDocumentExpression Parse(ValueExpression json) => new(InvokeStatic(nameof(JsonDocument.Parse), json)); - public static JsonDocumentExpression Parse(BinaryDataExpression binaryData) => new(InvokeStatic(nameof(JsonDocument.Parse), binaryData)); - public static JsonDocumentExpression Parse(StreamExpression stream) => new(InvokeStatic(nameof(JsonDocument.Parse), stream)); - - public static JsonDocumentExpression Parse(StreamExpression stream, bool async) - { - // Sync and async methods have different set of parameters - return async - ? new JsonDocumentExpression(InvokeStatic(nameof(JsonDocument.ParseAsync), new[] { stream, Snippets.Default, Snippets.Default }, true)) - : new JsonDocumentExpression(InvokeStatic(nameof(JsonDocument.Parse), stream)); - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/JsonElementExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/JsonElementExpression.cs deleted file mode 100644 index cef7c3e4ca8..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/JsonElementExpression.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Text.Json; -using static Microsoft.Generator.CSharp.Expressions.Snippets; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record JsonElementExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) - { - public JsonValueKindExpression ValueKind => new(Property(nameof(JsonElement.ValueKind))); - public EnumerableExpression EnumerateArray() => new(typeof(JsonElement), Invoke(nameof(JsonElement.EnumerateArray))); - public EnumerableExpression EnumerateObject() => new(typeof(JsonProperty), Invoke(nameof(JsonElement.EnumerateObject))); - public JsonElementExpression this[int index] => new(new IndexerExpression(Untyped, Int(index))); - public JsonElementExpression GetProperty(string propertyName) => new(Invoke(nameof(JsonElement.GetProperty), Literal(propertyName))); - - public ValueExpression InvokeClone() => Invoke(nameof(JsonElement.Clone)); - public ValueExpression GetArrayLength() => Invoke(nameof(JsonElement.GetArrayLength)); - public ValueExpression GetBoolean() => Invoke(nameof(JsonElement.GetBoolean)); - public ValueExpression GetBytesFromBase64() => Invoke(nameof(JsonElement.GetBytesFromBase64)); - public ValueExpression GetDateTime() => Invoke(nameof(JsonElement.GetDateTime)); - public ValueExpression GetDecimal() => Invoke(nameof(JsonElement.GetDecimal)); - public ValueExpression GetDouble() => Invoke(nameof(JsonElement.GetDouble)); - public ValueExpression GetGuid() => Invoke(nameof(JsonElement.GetGuid)); - public ValueExpression GetSByte() => Invoke(nameof(JsonElement.GetSByte)); - public ValueExpression GetByte() => Invoke(nameof(JsonElement.GetByte)); - public ValueExpression GetInt16() => Invoke(nameof(JsonElement.GetInt16)); - public ValueExpression GetInt32() => Invoke(nameof(JsonElement.GetInt32)); - public ValueExpression GetInt64() => Invoke(nameof(JsonElement.GetInt64)); - public StringExpression GetRawText() => new(Invoke(nameof(JsonElement.GetRawText))); - public ValueExpression GetSingle() => Untyped.Invoke(nameof(JsonElement.GetSingle)); - public StringExpression GetString() => new(Untyped.Invoke(nameof(JsonElement.GetString))); - - public BoolExpression ValueKindEqualsNull() - => new(new BinaryOperatorExpression("==", Property(nameof(JsonElement.ValueKind)), FrameworkEnumValue(JsonValueKind.Null))); - - public BoolExpression ValueKindNotEqualsUndefined() - => new(new BinaryOperatorExpression("!=", Property(nameof(JsonElement.ValueKind)), FrameworkEnumValue(JsonValueKind.Undefined))); - - public BoolExpression ValueKindEqualsString() - => new(new BinaryOperatorExpression("==", Property(nameof(JsonElement.ValueKind)), FrameworkEnumValue(JsonValueKind.String))); - - public MethodBodyStatement WriteTo(ValueExpression writer) => new InvokeInstanceMethodStatement(Untyped, nameof(JsonElement.WriteTo), new[] { writer }, false); - - public BoolExpression TryGetProperty(string propertyName, out JsonElementExpression discriminator) - { - var discriminatorDeclaration = new VariableReference(typeof(JsonElement), "discriminator"); - discriminator = new JsonElementExpression(discriminatorDeclaration); - var invocation = new InvokeInstanceMethodExpression(this, nameof(JsonElement.TryGetProperty), new ValueExpression[] { Literal(propertyName), new DeclarationExpression(discriminatorDeclaration, true) }, null, false); - return new BoolExpression(invocation); - } - - public BoolExpression TryGetInt32(out IntExpression intValue) - { - var intValueDeclaration = new VariableReference(typeof(int), "intValue"); - intValue = new IntExpression(intValueDeclaration); - var invocation = new InvokeInstanceMethodExpression(this, nameof(JsonElement.TryGetInt32), new ValueExpression[] { new DeclarationExpression(intValueDeclaration, true) }, null, false); - return new BoolExpression(invocation); - } - - public BoolExpression TryGetInt64(out LongExpression longValue) - { - var longValueDeclaration = new VariableReference(typeof(long), "longValue"); - longValue = new LongExpression(longValueDeclaration); - var invocation = new InvokeInstanceMethodExpression(this, nameof(JsonElement.TryGetInt64), new ValueExpression[] { new DeclarationExpression(longValueDeclaration, true) }, null, false); - return new BoolExpression(invocation); - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/JsonPropertyExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/JsonPropertyExpression.cs deleted file mode 100644 index fa5d9c080b3..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/JsonPropertyExpression.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Text.Json; -using static Microsoft.Generator.CSharp.Expressions.Snippets; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record JsonPropertyExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) - { - public StringExpression Name => new(Property(nameof(JsonProperty.Name))); - public JsonElementExpression Value => new(Property(nameof(JsonProperty.Value))); - - public BoolExpression NameEquals(string value) => new(Invoke(nameof(JsonProperty.NameEquals), LiteralU8(value))); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/JsonSerializerExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/JsonSerializerExpression.cs deleted file mode 100644 index c63bbb5974a..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/JsonSerializerExpression.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Text.Json; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public static class JsonSerializerExpression - { - public static InvokeStaticMethodExpression Serialize(ValueExpression writer, ValueExpression value, ValueExpression? options = null) - { - var arguments = options is null - ? new[] { writer, value } - : new[] { writer, value, options }; - return new InvokeStaticMethodExpression(typeof(JsonSerializer), nameof(JsonSerializer.Serialize), arguments); - } - - public static InvokeStaticMethodExpression Deserialize(JsonElementExpression element, CSharpType serializationType, ValueExpression? options = null) - { - var arguments = options is null - ? new[] { element.GetRawText() } - : new[] { element.GetRawText(), options }; - return new InvokeStaticMethodExpression(typeof(JsonSerializer), nameof(JsonSerializer.Deserialize), arguments, new[] { serializationType }); - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/JsonValueKindExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/JsonValueKindExpression.cs deleted file mode 100644 index e38861f38b5..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/JsonValueKindExpression.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Text.Json; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record JsonValueKindExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) - { - public static JsonValueKindExpression String => InvokeStaticProperty(nameof(JsonValueKind.String)); - public static JsonValueKindExpression Number => InvokeStaticProperty(nameof(JsonValueKind.Number)); - public static JsonValueKindExpression True => InvokeStaticProperty(nameof(JsonValueKind.True)); - public static JsonValueKindExpression False => InvokeStaticProperty(nameof(JsonValueKind.False)); - public static JsonValueKindExpression Undefined => InvokeStaticProperty(nameof(JsonValueKind.Undefined)); - public static JsonValueKindExpression Null => InvokeStaticProperty(nameof(JsonValueKind.Null)); - public static JsonValueKindExpression Array => InvokeStaticProperty(nameof(JsonValueKind.Array)); - public static JsonValueKindExpression Object => InvokeStaticProperty(nameof(JsonValueKind.Object)); - - private static JsonValueKindExpression InvokeStaticProperty(string name) - => new(new MemberExpression(typeof(JsonValueKind), name)); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/KeyValuePairExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/KeyValuePairExpression.cs deleted file mode 100644 index b29fbfc8e2c..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/KeyValuePairExpression.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections.Generic; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record KeyValuePairExpression(CSharpType KeyType, CSharpType ValueType, ValueExpression Untyped) : TypedValueExpression(GetType(KeyType, ValueType), Untyped) - { - public TypedValueExpression Key => new TypedMemberExpression(Untyped, nameof(KeyValuePair.Key), KeyType); - public TypedValueExpression Value => new TypedMemberExpression(Untyped, nameof(KeyValuePair.Value), ValueType); - - public static CSharpType GetType(CSharpType keyType, CSharpType valueType) => new(typeof(KeyValuePair<,>), false, keyType, valueType); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/KeywordExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/KeywordExpression.cs index 17ee2d0dfdc..7adfcbde319 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/KeywordExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/KeywordExpression.cs @@ -1,11 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Generator.CSharp.Expressions { public sealed record KeywordExpression(string Keyword, ValueExpression? Expression) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { writer.AppendRaw(Keyword); if (Expression is not null) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ListExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ListExpression.cs deleted file mode 100644 index e65ebf311b7..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ListExpression.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections.Generic; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record ListExpression(CSharpType ItemType, ValueExpression Untyped) : TypedValueExpression(new CSharpType(typeof(List<>), ItemType), Untyped) - { - public MethodBodyStatement Add(ValueExpression item) => new InvokeInstanceMethodStatement(Untyped, nameof(List.Add), item); - - public ValueExpression ToArray() => Invoke(nameof(List.ToArray)); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/LiteralExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/LiteralExpression.cs index ee0fbbf5fe8..b6a8d5962cb 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/LiteralExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/LiteralExpression.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using Microsoft.CodeAnalysis.CSharp; +using System; + namespace Microsoft.Generator.CSharp.Expressions { /// @@ -9,9 +12,22 @@ namespace Microsoft.Generator.CSharp.Expressions /// The literal value. public sealed record LiteralExpression(object? Literal) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { - writer.WriteLiteral(Literal); + writer.AppendRaw(Literal switch + { + null => "null", + string s => SyntaxFactory.Literal(s).ToString(), + int i => SyntaxFactory.Literal(i).ToString(), + long l => SyntaxFactory.Literal(l).ToString(), + decimal d => SyntaxFactory.Literal(d).ToString(), + double d => SyntaxFactory.Literal(d).ToString(), + float f => SyntaxFactory.Literal(f).ToString(), + char c => SyntaxFactory.Literal(c).ToString(), + bool b => b ? "true" : "false", + BinaryData bd => bd.ToArray().Length == 0 ? "new byte[] { }" : SyntaxFactory.Literal(bd.ToString()).ToString(), + _ => throw new NotImplementedException() + }); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/LongExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/LongExpression.cs deleted file mode 100644 index a34a822c047..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/LongExpression.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record LongExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) - { - public StringExpression InvokeToString(ValueExpression formatProvider) - => new(Invoke(nameof(long.ToString), formatProvider)); - - public static LongExpression Parse(StringExpression value, ValueExpression formatProvider) - => new(new InvokeStaticMethodExpression(typeof(long), nameof(long.Parse), new[] { value, formatProvider })); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/MemberExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/MemberExpression.cs index ead594e97ca..fb36ee9e553 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/MemberExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/MemberExpression.cs @@ -1,11 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Generator.CSharp.Expressions { public sealed record MemberExpression(ValueExpression? Inner, string MemberName) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { if (Inner is not null) { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NewArrayExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NewArrayExpression.cs index 7bb87b5ae08..1ed52d18b24 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NewArrayExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NewArrayExpression.cs @@ -1,11 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Generator.CSharp.Expressions { public sealed record NewArrayExpression(CSharpType? Type, ArrayInitializerExpression? Items = null, ValueExpression? Size = null) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { if (Size is not null) { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NewDictionaryExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NewDictionaryExpression.cs index ba5e28f40bc..b66040f7549 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NewDictionaryExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NewDictionaryExpression.cs @@ -1,11 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Generator.CSharp.Expressions { public sealed record NewDictionaryExpression(CSharpType Type, DictionaryInitializerExpression? Values = null) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { writer.Append($"new {Type}"); if (Values is { Values.Count: > 0 }) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NewInstanceExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NewInstanceExpression.cs index 64e3c9ca22a..774877cf69a 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NewInstanceExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NewInstanceExpression.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections.Generic; @@ -9,7 +9,7 @@ public sealed record NewInstanceExpression(CSharpType Type, IReadOnlyList 0 || InitExpression is not { Parameters.Count: > 0 }) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NewJsonSerializerOptionsExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NewJsonSerializerOptionsExpression.cs deleted file mode 100644 index b0829554070..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NewJsonSerializerOptionsExpression.cs +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record NewJsonSerializerOptionsExpression : ValueExpression { } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NullConditionalExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NullConditionalExpression.cs index edfb2c9025c..9399fb685cf 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NullConditionalExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/NullConditionalExpression.cs @@ -1,11 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Generator.CSharp.Expressions { public sealed record NullConditionalExpression(ValueExpression Inner) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { Inner.Write(writer); writer.AppendRaw("?"); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ObjectInitializerExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ObjectInitializerExpression.cs index 67bead7b02f..3e50b2c1adc 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ObjectInitializerExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ObjectInitializerExpression.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections.Generic; @@ -10,9 +10,9 @@ namespace Microsoft.Generator.CSharp.Expressions /// /// The parameters to initialize the object to. /// Flag to determine if the object should be initialized inline. - public sealed record ObjectInitializerExpression(IReadOnlyDictionary? Parameters = null, bool UseSingleLine = true) : InitializerExpression + public sealed record ObjectInitializerExpression(IReadOnlyDictionary? Parameters = null, bool UseSingleLine = true) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { if (Parameters is not { Count: > 0 }) { @@ -23,14 +23,20 @@ public override void Write(CodeWriter writer) if (UseSingleLine) { writer.AppendRaw("{"); - foreach (var (name, value) in Parameters) + var iterator = Parameters.GetEnumerator(); + if (iterator.MoveNext()) { + var (name, value) = iterator.Current; writer.Append($"{name} = "); value.Write(writer); - writer.AppendRaw(", "); + while (iterator.MoveNext()) + { + writer.AppendRaw(", "); + (name, value) = iterator.Current; + writer.Append($"{name} = "); + value.Write(writer); + } } - - writer.RemoveTrailingComma(); writer.AppendRaw("}"); } else diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ObjectTypeExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ObjectTypeExpression.cs deleted file mode 100644 index f406c13a34e..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ObjectTypeExpression.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record ObjectTypeExpression(TypeProvider TypeProvider, ValueExpression Untyped) : TypedValueExpression(TypeProvider.Type, Untyped) - { - public static MemberExpression FromResponseDelegate(TypeProvider typeProvider) - => new(new TypeReference(typeProvider.Type), CodeModelPlugin.Instance.Configuration.ApiTypes.FromResponseName); - - public static MemberExpression DeserializeDelegate(TypeProvider typeProvider) - => new(new TypeReference(typeProvider.Type), $"Deserialize{typeProvider.Name}"); - - public static ObjectTypeExpression Deserialize(TypeProvider typeProvider, ValueExpression element, ValueExpression? options = null) - { - var arguments = options == null ? new[] { element } : new[] { element, options }; - return new(typeProvider, new InvokeStaticMethodExpression(typeProvider.Type, $"Deserialize{typeProvider.Name}", arguments)); - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ParameterReference.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ParameterReference.cs deleted file mode 100644 index 11bddb7051e..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ParameterReference.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record ParameterReference(Parameter Parameter) : TypedValueExpression(Parameter.Type, new UntypedParameterReference(Parameter)) - { - private record UntypedParameterReference(Parameter Parameter) : ValueExpression - { - public override void Write(CodeWriter writer) - { - writer.AppendRawIf("ref ", Parameter.IsRef); - writer.Append($"{Parameter.Name:I}"); - } - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/PositionalParameterReference.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/PositionalParameterReference.cs deleted file mode 100644 index d06f57e632e..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/PositionalParameterReference.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record PositionalParameterReference(string ParameterName, ValueExpression ParameterValue) : ValueExpression - { - internal PositionalParameterReference(Parameter parameter) : this(parameter.Name, parameter) { } - - public override void Write(CodeWriter writer) - { - writer.Append($"{ParameterName}: "); - ParameterValue.Write(writer); - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/PositionalParameterReferenceExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/PositionalParameterReferenceExpression.cs new file mode 100644 index 00000000000..06b40a5d0e9 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/PositionalParameterReferenceExpression.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Generator.CSharp.Providers; + +namespace Microsoft.Generator.CSharp.Expressions +{ + public sealed record PositionalParameterReferenceExpression(string ParameterName, ValueExpression ParameterValue) : ValueExpression + { + internal PositionalParameterReferenceExpression(ParameterProvider parameter) : this(parameter.Name, parameter) { } + + internal override void Write(CodeWriter writer) + { + writer.Append($"{ParameterName}: "); + ParameterValue.Write(writer); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/StreamExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/StreamExpression.cs deleted file mode 100644 index 3aae8e83a28..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/StreamExpression.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.IO; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record StreamExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) - { - public MethodBodyStatement CopyTo(StreamExpression destination) => new InvokeInstanceMethodStatement(Untyped, nameof(Stream.CopyTo), destination); - - public ValueExpression Position => new TypedMemberExpression(this, nameof(Stream.Position), typeof(long)); - public ValueExpression GetBuffer => new InvokeInstanceMethodExpression(this, nameof(MemoryStream.GetBuffer), Array.Empty(), null, false); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/StreamReaderExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/StreamReaderExpression.cs deleted file mode 100644 index 92fe3e8e265..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/StreamReaderExpression.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.IO; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record StreamReaderExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) - { - public StringExpression ReadToEnd(bool async) - { - var methodName = async ? nameof(StreamReader.ReadToEndAsync) : nameof(StreamReader.ReadToEnd); - return new(Invoke(methodName, Array.Empty(), async)); - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/StringLiteralExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/StringLiteralExpression.cs deleted file mode 100644 index 51a6d8e8840..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/StringLiteralExpression.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Expressions -{ - /// - /// Represents a string literal expression. - /// - /// The string literal. - /// Flag used to determine if the string expression should represent a UTF-8 string. - public sealed record StringLiteralExpression(string Literal, bool U8) : ValueExpression - { - public override void Write(CodeWriter writer) - { - writer.WriteLiteral(Literal); - if (U8) - { - writer.AppendRaw("u8"); - } - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/SwitchCaseExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/SwitchCaseExpression.cs index 28756907646..ea6d6479a31 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/SwitchCaseExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/SwitchCaseExpression.cs @@ -1,6 +1,8 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using Microsoft.Generator.CSharp.Snippets; + namespace Microsoft.Generator.CSharp.Expressions { /// @@ -8,12 +10,19 @@ namespace Microsoft.Generator.CSharp.Expressions /// /// The conditional case. /// The expression. - public sealed record SwitchCaseExpression(ValueExpression Case, ValueExpression Expression) + public sealed record SwitchCaseExpression(ValueExpression Case, ValueExpression Expression) : ValueExpression { - public static SwitchCaseExpression When(ValueExpression caseExpression, BoolExpression condition, ValueExpression expression) + public static SwitchCaseExpression When(ValueExpression caseExpression, ValueExpression condition, ValueExpression expression) { return new(new SwitchCaseWhenExpression(caseExpression, condition), expression); } - public static SwitchCaseExpression Default(ValueExpression expression) => new SwitchCaseExpression(Snippets.Dash, expression); + public static SwitchCaseExpression Default(ValueExpression expression) => new SwitchCaseExpression(Snippet.Dash, expression); + + internal override void Write(CodeWriter writer) + { + Case.Write(writer); + writer.AppendRaw(" => "); + Expression.Write(writer); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/SwitchCaseWhenExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/SwitchCaseWhenExpression.cs index e136f555ecc..46884fbbd9c 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/SwitchCaseWhenExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/SwitchCaseWhenExpression.cs @@ -1,11 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Generator.CSharp.Expressions { - public sealed record SwitchCaseWhenExpression(ValueExpression Case, BoolExpression Condition) : ValueExpression + public sealed record SwitchCaseWhenExpression(ValueExpression Case, ValueExpression Condition) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { Case.Write(writer); writer.AppendRaw(" when "); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/SwitchExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/SwitchExpression.cs index 051110ad487..cfac85aa8f9 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/SwitchExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/SwitchExpression.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Generator.CSharp.Expressions @@ -10,21 +10,22 @@ namespace Microsoft.Generator.CSharp.Expressions /// The list of cases represented in the form of . public sealed record SwitchExpression(ValueExpression MatchExpression, params SwitchCaseExpression[] Cases) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { using (writer.AmbientScope()) { MatchExpression.Write(writer); writer.WriteRawLine(" switch"); - writer.WriteRawLine("{"); - foreach (var switchCase in Cases) + writer.AppendRaw("{"); + using (writer.ScopeRaw(string.Empty, string.Empty, false)) { - switchCase.Case.Write(writer); - writer.AppendRaw(" => "); - switchCase.Expression.Write(writer); - writer.WriteRawLine(","); + for (int i = 0; i < Cases.Length; i++) + { + Cases[i].Write(writer); + if (i < Cases.Length - 1) + writer.WriteRawLine(","); + } } - writer.RemoveTrailingComma(); writer.WriteLine(); writer.AppendRaw("}"); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TernaryConditionalOperator.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TernaryConditionalExpression.cs similarity index 59% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TernaryConditionalOperator.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TernaryConditionalExpression.cs index 64b57381596..9eee24480b6 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TernaryConditionalOperator.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TernaryConditionalExpression.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Generator.CSharp.Expressions @@ -6,9 +6,9 @@ namespace Microsoft.Generator.CSharp.Expressions /// /// Represents a ternary conditional operator. /// - public sealed record TernaryConditionalOperator(ValueExpression Condition, ValueExpression Consequent, ValueExpression Alternative) : ValueExpression + public sealed record TernaryConditionalExpression(ValueExpression Condition, ValueExpression Consequent, ValueExpression Alternative) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { Condition.Write(writer); writer.AppendRaw(" ? "); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TimeSpanExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TimeSpanExpression.cs deleted file mode 100644 index ae678b5ddb2..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TimeSpanExpression.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record TimeSpanExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) - { - public StringExpression InvokeToString(string? format) => new(Invoke(nameof(TimeSpan.ToString), new[] { Snippets.Literal(format) })); - public StringExpression InvokeToString(ValueExpression format, ValueExpression formatProvider) - => new(Invoke(nameof(TimeSpan.ToString), new[] { format, formatProvider })); - - public static TimeSpanExpression FromSeconds(ValueExpression value) => new(InvokeStatic(nameof(TimeSpan.FromSeconds), value)); - - public static TimeSpanExpression ParseExact(ValueExpression value, ValueExpression format, ValueExpression formatProvider) - => new(new InvokeStaticMethodExpression(typeof(TimeSpan), nameof(TimeSpan.ParseExact), new[] { value, format, formatProvider })); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypeReference.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypeReference.cs deleted file mode 100644 index a964c8b6e4e..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypeReference.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record TypeReference(CSharpType Type) : ValueExpression - { - public override void Write(CodeWriter writer) - { - writer.Append($"{Type}"); - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypeReferenceExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypeReferenceExpression.cs new file mode 100644 index 00000000000..8243da395d9 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypeReferenceExpression.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Generator.CSharp.Expressions +{ + public sealed record TypeReferenceExpression(CSharpType Type) : ValueExpression + { + internal override void Write(CodeWriter writer) + { + writer.Append($"{Type}"); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypedFuncExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypedFuncExpression.cs deleted file mode 100644 index 7fc0a9022dd..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypedFuncExpression.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections.Generic; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record TypedFuncExpression(IReadOnlyList Parameters, TypedValueExpression Inner) : TypedValueExpression(Inner.Type, new FuncExpression(Parameters, Inner)); -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypedMemberExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypedMemberExpression.cs deleted file mode 100644 index a2d79a5c8c4..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypedMemberExpression.cs +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Expressions -{ - internal record TypedMemberExpression(ValueExpression? Inner, string MemberName, CSharpType MemberType) : TypedValueExpression(MemberType, new MemberExpression(Inner, MemberName)); -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypedNullConditionalExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypedNullConditionalExpression.cs deleted file mode 100644 index 366519b3009..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypedNullConditionalExpression.cs +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Expressions -{ - internal record TypedNullConditionalExpression(TypedValueExpression Inner) : TypedValueExpression(Inner.Type, new NullConditionalExpression(Inner.Untyped)); -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypedTernaryConditionalOperator.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypedTernaryConditionalOperator.cs deleted file mode 100644 index f036bad913f..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypedTernaryConditionalOperator.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Expressions -{ - internal record TypedTernaryConditionalOperator(BoolExpression Condition, TypedValueExpression Consequent, ValueExpression Alternative) - : TypedValueExpression(Consequent.Type, new TernaryConditionalOperator(Condition, Consequent, Alternative)); -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypedValueExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypedValueExpression.cs deleted file mode 100644 index 7cf9a2b8e03..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypedValueExpression.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.Generator.CSharp.Expressions -{ - /// - /// A wrapper around ValueExpression that also specifies return type of the expression - /// Return type doesn't affect how expression is written, but helps creating strong-typed helpers to create value expressions. - /// - /// Type expected to be returned by value expression. - /// - public abstract record TypedValueExpression(CSharpType Type, ValueExpression Untyped) : ValueExpression - { - public override void Write(CodeWriter writer) => Untyped.Write(writer); - public static implicit operator TypedValueExpression(FieldDeclaration field) => new TypedMemberExpression(null, field.Name, field.Type); - public static implicit operator TypedValueExpression(PropertyDeclaration property) => new TypedMemberExpression(null, property.Name, property.Type); - public static implicit operator TypedValueExpression(Parameter parameter) => new ParameterReference(parameter); - - public TypedValueExpression NullableStructValue() => Type is { IsNullable: true, IsValueType: true } ? new TypedMemberExpression(this, nameof(Nullable.Value), Type.WithNullable(false)) : this; - - public TypedValueExpression NullConditional() => Type.IsNullable ? new TypedNullConditionalExpression(this) : this; - - public virtual TypedValueExpression Property(string propertyName, CSharpType propertyType) - => new TypedMemberExpression(this, propertyName, propertyType); - - protected static ValueExpression ValidateType(TypedValueExpression typed, CSharpType type) - { - if (type.Equals(typed.Type, ignoreNullable: true)) - { - return typed.Untyped; - } - - throw new InvalidOperationException($"Expression with return type {typed.Type.Name} is cast to type {type.Name}"); - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/UnaryOperatorExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/UnaryOperatorExpression.cs index ced65a70bbf..5d662db1f74 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/UnaryOperatorExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/UnaryOperatorExpression.cs @@ -1,11 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Generator.CSharp.Expressions { public sealed record UnaryOperatorExpression(string Operator, ValueExpression Operand, bool OperandOnTheLeft) : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { writer.AppendRawIf(Operator, !OperandOnTheLeft); Operand.Write(writer); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ValueExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ValueExpression.cs index 620bb01e79f..1376ce6d978 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ValueExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/ValueExpression.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Snippets; namespace Microsoft.Generator.CSharp.Expressions { @@ -12,19 +14,20 @@ namespace Microsoft.Generator.CSharp.Expressions /// public record ValueExpression { - public virtual void Write(CodeWriter writer) { } + internal virtual void Write(CodeWriter writer) { } - public static implicit operator ValueExpression(Type type) => new TypeReference(type); - public static implicit operator ValueExpression(CSharpType type) => new TypeReference(type); - public static implicit operator ValueExpression(Parameter parameter) => new ParameterReference(parameter); - public static implicit operator ValueExpression(FieldDeclaration field) => new MemberExpression(null, field.Name); - public static implicit operator ValueExpression(PropertyDeclaration property) => new MemberExpression(null, property.Name); + public static implicit operator ValueExpression(Type type) => new TypeReferenceExpression(type); + public static implicit operator ValueExpression(CSharpType type) => new TypeReferenceExpression(type); + public static implicit operator ValueExpression(ParameterProvider parameter) => new ParameterReferenceSnippet(parameter); + public static implicit operator ValueExpression(FieldProvider field) => new MemberExpression(null, field.Name); + public static implicit operator ValueExpression(PropertyProvider property) => new MemberExpression(null, property.Name); public ValueExpression NullableStructValue(CSharpType candidateType) => candidateType is { IsNullable: true, IsValueType: true } ? new MemberExpression(this, nameof(Nullable.Value)) : this; - public StringExpression InvokeToString() => new(Invoke(nameof(ToString))); + public StringSnippet InvokeToString() => new(Invoke(nameof(ToString))); public ValueExpression InvokeGetType() => Invoke(nameof(GetType)); + public ValueExpression InvokeGetHashCode() => Invoke(nameof(GetHashCode)); - public BoolExpression InvokeEquals(ValueExpression other) => new(Invoke(nameof(Equals), other)); + public BoolSnippet InvokeEquals(ValueExpression other) => new(Invoke(nameof(Equals), other)); public virtual ValueExpression Property(string propertyName, bool nullConditional = false) => new MemberExpression(nullConditional ? new NullConditionalExpression(this) : this, propertyName); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/VariableReference.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/VariableReference.cs deleted file mode 100644 index e00e6cb96d9..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/VariableReference.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record VariableReference(CSharpType Type, CodeWriterDeclaration Declaration) : TypedValueExpression(Type, new FormattableStringToExpression($"{Declaration:I}")) - { - public VariableReference(CSharpType type, string name) : this(type, new CodeWriterDeclaration(name)) { } - - private record UntypedVariableReference(CodeWriterDeclaration Declaration) : ValueExpression - { - public override void Write(CodeWriter writer) - { - writer.Append(Declaration); - } - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/WhereExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/WhereExpression.cs index 5be16e5e841..a64e69b13ad 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/WhereExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/WhereExpression.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections.Generic; @@ -11,17 +11,17 @@ public WhereExpression(CSharpType type, ValueExpression constraint) : this(type, public WhereExpression And(ValueExpression constraint) => new(Type, new List(Constraints) { constraint }); - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { writer .AppendRaw("where ") .Append($"{Type} : "); - foreach (var constraint in Constraints) + for (int i = 0; i < Constraints.Count; i++) { - constraint.Write(writer); - writer.AppendRaw(","); + Constraints[i].Write(writer); + if (i < Constraints.Count - 1) + writer.AppendRaw(", "); } - writer.RemoveTrailingComma(); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/AutoPropertyBody.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/AutoPropertyBody.cs index 698b3e4c7fc..23c875dff3f 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/AutoPropertyBody.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/AutoPropertyBody.cs @@ -5,5 +5,5 @@ namespace Microsoft.Generator.CSharp { - internal record AutoPropertyBody(bool HasSetter, MethodSignatureModifiers SetterModifiers = MethodSignatureModifiers.None, ValueExpression? InitializationExpression = null) : PropertyBody; + internal record AutoPropertyBody(bool HasSetter, MethodSignatureModifiers SetterModifiers = MethodSignatureModifiers.None, ValueExpression? InitializationExpression = null) : PropertyBody(HasSetter); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/CSharpAttribute.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/CSharpAttribute.cs deleted file mode 100644 index e0875a3e24c..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/CSharpAttribute.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections.Generic; -using Microsoft.Generator.CSharp.Expressions; - -namespace Microsoft.Generator.CSharp -{ - public sealed record CSharpAttribute(CSharpType Type, IReadOnlyList Arguments) - { - public CSharpAttribute(CSharpType type, params ValueExpression[] arguments) : this(type, (IReadOnlyList)arguments) { } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/CSharpMethodCollection.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/CSharpMethodCollection.cs index eef90a19910..2e3cb985bf8 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/CSharpMethodCollection.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/CSharpMethodCollection.cs @@ -4,21 +4,23 @@ using System; using System.Collections; using System.Collections.Generic; -using Microsoft.Generator.CSharp.Expressions; using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Statements; +using Microsoft.Generator.CSharp.Snippets; namespace Microsoft.Generator.CSharp { /// /// Represents an immutable collection of methods that are associated with an operation . /// - public sealed class CSharpMethodCollection : IReadOnlyList + public sealed class CSharpMethodCollection : IReadOnlyList { - private readonly IReadOnlyList _cSharpMethods; + private readonly IReadOnlyList _cSharpMethods; - private CSharpMethodCollection(IReadOnlyList methods) + private CSharpMethodCollection(IReadOnlyList methods) { - _cSharpMethods = methods ?? Array.Empty(); + _cSharpMethods = methods ?? Array.Empty(); } /// @@ -29,12 +31,12 @@ private CSharpMethodCollection(IReadOnlyList methods) public static CSharpMethodCollection DefaultCSharpMethodCollection(InputOperation operation) { var createMessageMethod = BuildCreateMessageMethod(operation); - var cSharpMethods = new List() { createMessageMethod }; + var cSharpMethods = new List() { createMessageMethod }; // TO-DO: Add Protocol and Convenience methods https://github.com/Azure/autorest.csharp/issues/4585, https://github.com/Azure/autorest.csharp/issues/4586 return new CSharpMethodCollection(cSharpMethods); } - public CSharpMethod this[int index] + public MethodProvider this[int index] { get { return _cSharpMethods[index]; } } @@ -44,10 +46,10 @@ public int Count get { return _cSharpMethods.Count; } } - private static CSharpMethod BuildCreateMessageMethod(InputOperation operation) + private static MethodProvider BuildCreateMessageMethod(InputOperation operation) { // TO-DO: properly build method https://github.com/Azure/autorest.csharp/issues/4583 - List methodParameters = new(); + List methodParameters = new(); foreach (var inputParam in operation.Parameters) { methodParameters.Add(CodeModelPlugin.Instance.TypeFactory.CreateCSharpParam(inputParam)); @@ -56,17 +58,17 @@ private static CSharpMethod BuildCreateMessageMethod(InputOperation operation) var methodModifier = MethodSignatureModifiers.Internal; var methodSignatureName = $"Create{operation.Name.ToCleanName()}Request"; var methodSignature = new MethodSignature(methodSignatureName, FormattableStringHelpers.FromString(operation.Summary), FormattableStringHelpers.FromString(operation.Description), methodModifier, null, null, Parameters: methodParameters); - var methodBody = new EmptyLineStatement(); + var methodBody = Snippet.EmptyStatement; - return new CSharpMethod(methodSignature, methodBody, CSharpMethodKinds.CreateMessage); + return new MethodProvider(methodSignature, methodBody, CSharpMethodKinds.CreateMessage); } /// /// Returns all methods in the collection of a specific kind. /// - internal List GetMethods(CSharpMethodKinds kind) + internal List GetMethods(CSharpMethodKinds kind) { - var methods = new List(); + var methods = new List(); foreach (var method in _cSharpMethods) { if (method.Kind == kind) @@ -78,7 +80,7 @@ internal List GetMethods(CSharpMethodKinds kind) return methods; } - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { return _cSharpMethods.GetEnumerator(); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/CSharpMethodKinds.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/CSharpMethodKinds.cs index 1d3645ca36e..6949e2c11d7 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/CSharpMethodKinds.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/CSharpMethodKinds.cs @@ -8,14 +8,14 @@ namespace Microsoft.Generator.CSharp { /// - /// Represents the kind of . + /// Represents the kind of . /// public readonly partial struct CSharpMethodKinds : IEquatable { - internal const string ConvenienceValue = "Convenience"; - internal const string ProtocolValue = "Protocol"; - internal const string CreateMessageValue = "CreateMessage"; - internal const string ConstructorValue = "Constructor"; + private const string ConvenienceValue = "Convenience"; + private const string ProtocolValue = "Protocol"; + private const string CreateMessageValue = "CreateMessage"; + private const string ConstructorValue = "Constructor"; private readonly string _value; diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/CSharpType.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/CSharpType.cs index f3fc51a51e9..bab9c08fcb5 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/CSharpType.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/CSharpType.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Microsoft.Generator.CSharp.Providers; namespace Microsoft.Generator.CSharp { @@ -42,7 +43,6 @@ public class CSharpType private bool? _isIAsyncEnumerableOfT; private bool? _containsBinaryData; private int? _hashCode; - private CSharpType? _initializationType; private CSharpType? _propertyInitializationType; private CSharpType? _elementType; private CSharpType? _inputType; @@ -66,8 +66,8 @@ public class CSharpType /// Optional flag to determine if the constructed type should be nullable. Defaults to false. public CSharpType(Type type, bool isNullable = false) : this( type, - isNullable, - type.IsGenericType ? type.GetGenericArguments().Select(p => new CSharpType(p)).ToArray() : Array.Empty()) + type.IsGenericType ? type.GetGenericArguments().Select(p => new CSharpType(p)).ToArray() : Array.Empty(), + isNullable) { } /// @@ -97,6 +97,16 @@ public CSharpType(Type type, IReadOnlyList arguments, bool isNullabl { Debug.Assert(type.Namespace != null, "type.Namespace != null"); + // handle nullable value types explicitly because they are implemented using generic type `System.Nullable` + var underlyingValueType = Nullable.GetUnderlyingType(type); + if (underlyingValueType != null) + { + // in this block, we are converting input like `typeof(int?)` into the way as if they input: `typeof(int), isNullable: true` + type = underlyingValueType; + arguments = type.IsGenericType ? type.GetGenericArguments().Select(p => new CSharpType(p)).ToArray() : Array.Empty(); + isNullable = true; + } + _type = type.IsGenericType ? type.GetGenericTypeDefinition() : type; ValidateArguments(_type, arguments); @@ -190,8 +200,13 @@ private void Initialize(string? name, bool isValueType, bool isEnum, bool isNull public object Literal => _literal ?? throw new InvalidOperationException("Not a literal type"); internal TypeProvider Implementation => _implementation ?? throw new InvalidOperationException($"Not implemented type: '{Namespace}.{Name}'"); public IReadOnlyList Arguments { get { return _arguments; } } - public CSharpType InitializationType => _initializationType ??= GetImplementationType(); - public CSharpType PropertyInitializationType => _propertyInitializationType ??= GetPropertyImplementationType(); + + /// + /// Retrieves the property initialization type variant of this type. + /// For majority of the types, the return value of PropertyInitializationType should just be itself. + /// For special cases like interface types, such as collections, this will return the concrete implementation type. + /// + public CSharpType PropertyInitializationType => _propertyInitializationType ??= GetPropertyInitializationType(); public CSharpType ElementType => _elementType ??= GetElementType(); public CSharpType InputType => _inputType ??= GetInputType(); public CSharpType OutputType => _outputType ??= GetOutputType(); @@ -236,37 +251,10 @@ private bool TypeContainsBinaryData() } /// - /// Retrieves the implementation type for the . + /// Retrieves the initialization type with the . /// /// The implementation type . - private CSharpType GetImplementationType() - { - if (IsFrameworkType) - { - if (IsReadOnlyMemory) - { - return new CSharpType(Arguments[0].FrameworkType.MakeArrayType()); - } - - if (IsList) - { - return new CSharpType(typeof(List<>), Arguments); - } - - if (IsDictionary) - { - return new CSharpType(typeof(Dictionary<,>), Arguments); - } - } - - return this; - } - - /// - /// Retrieves the implementation type for the . - /// - /// The implementation type . - private CSharpType GetPropertyImplementationType() + private CSharpType GetPropertyInitializationType() { if (IsFrameworkType) { @@ -277,16 +265,12 @@ private CSharpType GetPropertyImplementationType() if (IsList) { - return new CSharpType(typeof(List<>), Arguments); - // Generate ChangeTrackingList type - https://github.com/microsoft/typespec/issues/3324 - // return new CSharpType(CodeModelPlugin.Instance.Configuration.ApiTypes.ChangeTrackingListType, Arguments); + return CodeModelPlugin.Instance.TypeFactory.ListInitializationType.MakeGenericType(Arguments); } if (IsDictionary) { - return new CSharpType(typeof(Dictionary<,>), Arguments); - // Generate ChangeTrackingDictionary type - https://github.com/microsoft/typespec/issues/3324 - //return new CSharpType(CodeModelPlugin.Instance.Configuration.ApiTypes.ChangeTrackingDictionaryType, Arguments); + return CodeModelPlugin.Instance.TypeFactory.DictionaryInitializationType.MakeGenericType(Arguments); } } @@ -439,7 +423,7 @@ internal bool TryGetCSharpFriendlyName([MaybeNullWhen(false)] out string name) /// Method checks if object of "from" type can be converted to "to" type by calling `ToList` extension method. /// It returns true if "from" is and "to" is or . /// - internal static bool RequiresToList(CSharpType from, CSharpType to) + public static bool RequiresToList(CSharpType from, CSharpType to) { if (!to.IsFrameworkType || !from.IsFrameworkType || from.FrameworkType != typeof(IEnumerable<>)) { @@ -517,7 +501,8 @@ public CSharpType WithNullable(bool isNullable) public sealed override string ToString() { - return new CodeWriter().Append($"{this}").ToString(false); + using var writer = new CodeWriter(); + return writer.Append($"{this}").ToString(false); } /// @@ -585,12 +570,13 @@ public static CSharpType FromLiteral(CSharpType type, object literalValue) return literalType; } - else if (type is { IsFrameworkType: false, Implementation: EnumTypeProvider enumType }) + else if (type is { IsFrameworkType: false, Implementation: EnumProvider enumType }) { var literalType = new CSharpType(enumType, type.Arguments, type.IsNullable) { _literal = literalValue }; + return literalType; } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ConstructorInitializer.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ConstructorInitializer.cs index f424e8f4ecb..969053227f9 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ConstructorInitializer.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ConstructorInitializer.cs @@ -4,11 +4,12 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Providers; namespace Microsoft.Generator.CSharp { public sealed record ConstructorInitializer(bool IsBase, IReadOnlyList Arguments) { - public ConstructorInitializer(bool isBase, IEnumerable arguments) : this(isBase, arguments.Select(p => (ValueExpression)p).ToArray()) { } + public ConstructorInitializer(bool isBase, IEnumerable arguments) : this(isBase, arguments.Select(p => (ValueExpression)p).ToArray()) { } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ConstructorSignature.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ConstructorSignature.cs index a3a9ffb4784..4667e9b00a3 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ConstructorSignature.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ConstructorSignature.cs @@ -1,8 +1,10 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; using System.Collections.Generic; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Statements; namespace Microsoft.Generator.CSharp { @@ -21,8 +23,8 @@ public sealed record ConstructorSignature( FormattableString? Summary, FormattableString? Description, MethodSignatureModifiers Modifiers, - IReadOnlyList Parameters, - IReadOnlyList? Attributes = null, + IReadOnlyList Parameters, + IReadOnlyList? Attributes = null, ConstructorInitializer? Initializer = null) - : MethodSignatureBase(Type.Name, Summary, Description, null, Modifiers, Parameters, Attributes ?? Array.Empty()); + : MethodSignatureBase(Type.Name, Summary, Description, null, Modifiers, Parameters, Attributes ?? Array.Empty()); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/Diagnostic.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/Diagnostic.cs deleted file mode 100644 index fd3e08246ab..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/Diagnostic.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.Generator.CSharp -{ - public class Diagnostic - { - public Diagnostic(string scopeName, DiagnosticAttribute[]? attributes = null) - { - ScopeName = scopeName; - Attributes = attributes ?? Array.Empty(); - } - - public string ScopeName { get; } - public DiagnosticAttribute[] Attributes { get; } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/EnumTypeMember.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/EnumTypeMember.cs new file mode 100644 index 00000000000..0790e35bd56 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/EnumTypeMember.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Generator.CSharp.Providers; + +namespace Microsoft.Generator.CSharp +{ + public class EnumTypeMember + { + public EnumTypeMember(string name, FieldProvider field, object value) + { + Name = name; + Field = field; + Value = value; + } + + public string Name { get; } + + public FieldProvider Field { get; } + + public object Value { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/EnumTypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/EnumTypeProvider.cs deleted file mode 100644 index fe53697915b..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/EnumTypeProvider.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using Microsoft.CodeAnalysis; -using Microsoft.Generator.CSharp.Input; - -namespace Microsoft.Generator.CSharp -{ - public class EnumTypeProvider : TypeProvider - { - private readonly InputEnumType _inputEnum; - private readonly IEnumerable _allowedValues; - private readonly ModelTypeMapping? _typeMapping; - private readonly TypeFactory _typeFactory; - - public EnumTypeProvider(InputEnumType input, string defaultNamespace, TypeFactory typeFactory, SourceInputModel? sourceInputModel) - : base(sourceInputModel) - { - _inputEnum = input; - _allowedValues = input.AllowedValues; - _typeFactory = typeFactory; - _deprecated = input.Deprecated; - - IsAccessibilityOverridden = input.Accessibility != null; - - var isExtensible = input.IsExtensible; - if (ExistingType != null) - { - isExtensible = ExistingType.TypeKind switch - { - TypeKind.Enum => false, - TypeKind.Struct => true, - _ => throw new InvalidOperationException( - $"{ExistingType.ToDisplayString()} cannot be mapped to enum," + - $" expected enum or struct got {ExistingType.TypeKind}") - }; - - _typeMapping = sourceInputModel?.CreateForModel(ExistingType); - } - - Name = input.Name.ToCleanName(); - IsExtensible = isExtensible; - ValueType = typeFactory.CreateCSharpType(input.EnumValueType); - IsStringValueType = ValueType.Equals(typeof(string)); - IsIntValueType = ValueType.Equals(typeof(int)) || ValueType.Equals(typeof(long)); - IsFloatValueType = ValueType.Equals(typeof(float)) || ValueType.Equals(typeof(double)); - IsNumericValueType = IsIntValueType || IsFloatValueType; - SerializationMethodName = IsStringValueType && IsExtensible ? "ToString" : $"ToSerial{ValueType.Name.FirstCharToUpperCase()}"; - } - - protected override TypeSignatureModifiers GetDeclarationModifiers() - { - var modifiers = TypeSignatureModifiers.Partial; - if (_inputEnum.Accessibility == "internal") - { - modifiers |= TypeSignatureModifiers.Internal; - } - - if (IsExtensible) - { - modifiers |= TypeSignatureModifiers.Struct; - } - else - { - modifiers |= TypeSignatureModifiers.Enum; - } - - return modifiers; - } - - public CSharpType ValueType { get; } - public bool IsExtensible { get; } - public bool IsIntValueType { get; } - public bool IsFloatValueType { get; } - public bool IsStringValueType { get; } - public bool IsNumericValueType { get; } - public string SerializationMethodName { get; } - public override string Name { get; } - public bool IsAccessibilityOverridden { get; } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ExpressionPropertyBody.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ExpressionPropertyBody.cs index 0aa884c7be4..d39d03778cd 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ExpressionPropertyBody.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ExpressionPropertyBody.cs @@ -5,5 +5,5 @@ namespace Microsoft.Generator.CSharp { - internal record ExpressionPropertyBody(ValueExpression Getter) : PropertyBody; + internal record ExpressionPropertyBody(ValueExpression Getter) : PropertyBody(false); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/FieldDeclaration.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/FieldDeclaration.cs deleted file mode 100644 index 2082af71533..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/FieldDeclaration.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using Microsoft.Generator.CSharp.Expressions; - -namespace Microsoft.Generator.CSharp -{ - public sealed record FieldDeclaration(FormattableString? Description, FieldModifiers Modifiers, CSharpType Type, string Name, ValueExpression? InitializationValue) - { - public string Accessibility => (Modifiers & FieldModifiers.Public) > 0 ? "public" : "internal"; - - public FieldDeclaration(FieldModifiers modifiers, CSharpType type, string name) - : this(description: null, - modifiers: modifiers, - type: type, - name: name) - { } - - public FieldDeclaration(FormattableString? description, FieldModifiers modifiers, CSharpType type, string name) - : this(Description: description, - Modifiers: modifiers, - Type: type, - Name: name, - InitializationValue: null) - { } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/IndexerDeclaration.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/IndexerDeclaration.cs deleted file mode 100644 index 80d84957aa2..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/IndexerDeclaration.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; - -namespace Microsoft.Generator.CSharp -{ - internal record IndexerDeclaration(FormattableString? Description, MethodSignatureModifiers Modifiers, CSharpType PropertyType, Parameter IndexerParameter, PropertyBody PropertyBody, IReadOnlyDictionary? Exceptions = null, CSharpType? ExplicitInterface = null) - : PropertyDeclaration(Description, Modifiers, PropertyType, "this", PropertyBody, Exceptions, ExplicitInterface); // the name of an indexer is always "this" -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/KnownParameters.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/KnownParameters.cs index 07df1adbf47..15b969da0c2 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/KnownParameters.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/KnownParameters.cs @@ -4,45 +4,54 @@ using System; using System.Runtime.CompilerServices; using System.Threading; -using Microsoft.Generator.CSharp.Expressions; -using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Snippets; +using Microsoft.Generator.CSharp.Statements; namespace Microsoft.Generator.CSharp { - public partial class KnownParameters + public static partial class KnownParameters { - public TypeFactory TypeFactory { get; } - public KnownParameters(TypeFactory typeFactory) - { - TypeFactory = typeFactory; - } + private static ParameterProvider? _tokenAuth; + public static ParameterProvider TokenAuth => _tokenAuth ??= new("tokenCredential", $"The token credential to copy", CodeModelPlugin.Instance.TypeFactory.TokenCredentialType()); - private static readonly CSharpType RequestContentType = new(CodeModelPlugin.Instance.Configuration.ApiTypes.RequestContentType); - private static readonly CSharpType RequestContentNullableType = new(CodeModelPlugin.Instance.Configuration.ApiTypes.RequestContentType, true); - private static readonly CSharpType RequestContextType = new(CodeModelPlugin.Instance.Configuration.ApiTypes.RequestContextType); - private static readonly CSharpType RequestContextNullableType = new(CodeModelPlugin.Instance.Configuration.ApiTypes.RequestContextType, true); - private static readonly CSharpType ResponseType = new(CodeModelPlugin.Instance.Configuration.ApiTypes.ResponseType); + private static ParameterProvider? _matchConditionsParameter; + public static ParameterProvider MatchConditionsParameter => _matchConditionsParameter ??= new("matchConditions", $"The content to send as the request conditions of the request.", CodeModelPlugin.Instance.TypeFactory.MatchConditionsType(), Snippet.DefaultOf(CodeModelPlugin.Instance.TypeFactory.RequestConditionsType())); - public Parameter TokenAuth => new("tokenCredential", $"The token credential to copy", TypeFactory.TokenCredentialType(), null, ValidationType.None, null); - public Parameter PageSizeHint => new("pageSizeHint", $"The number of items per {TypeFactory.PageResponseType():C} that should be requested (from service operations that support it). It's not guaranteed that the value will be respected.", new CSharpType(typeof(int), true), null, ValidationType.None, null); - public Parameter MatchConditionsParameter => new("matchConditions", $"The content to send as the request conditions of the request.", TypeFactory.MatchConditionsType(), Snippets.DefaultOf(TypeFactory.RequestConditionsType()), ValidationType.None, null, RequestLocation: RequestLocation.Header); - public Parameter RequestConditionsParameter => new("requestConditions", $"The content to send as the request conditions of the request.", TypeFactory.RequestConditionsType(), Snippets.DefaultOf(TypeFactory.RequestConditionsType()), ValidationType.None, null, RequestLocation: RequestLocation.Header); + private static ParameterProvider? _requestConditionsParameter; + public static ParameterProvider RequestConditionsParameter => _requestConditionsParameter ??= new("requestConditions", $"The content to send as the request conditions of the request.", CodeModelPlugin.Instance.TypeFactory.RequestConditionsType(), Snippet.DefaultOf(CodeModelPlugin.Instance.TypeFactory.RequestConditionsType())); - public static readonly Parameter Pipeline = new("pipeline", $"The HTTP pipeline for sending and receiving REST requests and responses", new CSharpType(CodeModelPlugin.Instance.Configuration.ApiTypes.HttpPipelineType), null, ValidationType.AssertNotNull, null); - public static readonly Parameter KeyAuth = new("keyCredential", $"The key credential to copy", new CSharpType(CodeModelPlugin.Instance.Configuration.ApiTypes.KeyCredentialType), null, ValidationType.None, null); - public static readonly Parameter Endpoint = new("endpoint", $"Service endpoint", new CSharpType(typeof(Uri)), null, ValidationType.None, null, RequestLocation: RequestLocation.Uri, IsEndpoint: true); + private static ParameterProvider? _pipeline; + public static ParameterProvider Pipeline => _pipeline ??= new("pipeline", $"The HTTP pipeline for sending and receiving REST requests and responses", new CSharpType(CodeModelPlugin.Instance.Configuration.ApiTypes.HttpPipelineType)); - public static readonly Parameter NextLink = new("nextLink", $"Continuation token", typeof(string), null, ValidationType.None, null); + private static ParameterProvider? _keyAuth; + public static ParameterProvider KeyAuth => _keyAuth ??= new("keyCredential", $"The key credential to copy", new CSharpType(CodeModelPlugin.Instance.Configuration.ApiTypes.KeyCredentialType)); - public static readonly Parameter RequestContent = new("content", $"The content to send as the body of the request.", RequestContentType, null, ValidationType.AssertNotNull, null, RequestLocation: RequestLocation.Body); - public static readonly Parameter RequestContentNullable = new("content", $"The content to send as the body of the request.", RequestContentNullableType, null, ValidationType.None, null, RequestLocation: RequestLocation.Body); + private static ParameterProvider? _endpoint; + public static ParameterProvider Endpoint => _endpoint ??= new("endpoint", $"Service endpoint", new CSharpType(typeof(Uri))); - public static readonly Parameter RequestContext = new("context", $"The request context, which can override default behaviors of the client pipeline on a per-call basis.", RequestContextNullableType, Snippets.DefaultOf(RequestContextNullableType), ValidationType.None, null); - public static readonly Parameter RequestContextRequired = new("context", $"The request context, which can override default behaviors of the client pipeline on a per-call basis.", RequestContextType, null, ValidationType.None, null); + private static ParameterProvider? _nextLink; + public static ParameterProvider NextLink => _nextLink ??= new("nextLink", $"Continuation token", typeof(string)); - public static readonly Parameter CancellationTokenParameter = new("cancellationToken", $"The cancellation token to use", new CSharpType(typeof(CancellationToken)), Snippets.DefaultOf(typeof(CancellationToken)), ValidationType.None, null); - public static readonly Parameter EnumeratorCancellationTokenParameter = new("cancellationToken", $"Enumerator cancellation token", typeof(CancellationToken), Snippets.DefaultOf(typeof(CancellationToken)), ValidationType.None, null) { Attributes = new[] { new CSharpAttribute(typeof(EnumeratorCancellationAttribute)) } }; + private static ParameterProvider? _requestContent; + public static ParameterProvider RequestContent => _requestContent ??= new("content", $"The content to send as the body of the request.", CodeModelPlugin.Instance.Configuration.ApiTypes.RequestContentType); - public static readonly Parameter Response = new("response", $"Response returned from backend service", ResponseType, null, ValidationType.None, null); + private static ParameterProvider? _requestContentNullable; + public static ParameterProvider RequestContentNullable => _requestContentNullable ??= new("content", $"The content to send as the body of the request.", new(CodeModelPlugin.Instance.Configuration.ApiTypes.RequestContentType, true)); + + private static ParameterProvider? _requestContext; + public static ParameterProvider RequestContext => _requestContext ??= new("context", $"The request context, which can override default behaviors of the client pipeline on a per-call basis.", new(CodeModelPlugin.Instance.Configuration.ApiTypes.RequestContextType, true), Snippet.DefaultOf(new(CodeModelPlugin.Instance.Configuration.ApiTypes.RequestContextType, true))); + + private static ParameterProvider? _requestContextRequired; + public static ParameterProvider RequestContextRequired => _requestContextRequired ??= new("context", $"The request context, which can override default behaviors of the client pipeline on a per-call basis.", CodeModelPlugin.Instance.Configuration.ApiTypes.RequestContextType); + + private static ParameterProvider? _cancellationTokenParameter; + public static ParameterProvider CancellationTokenParameter => _cancellationTokenParameter ??= new("cancellationToken", $"The cancellation token to use", new CSharpType(typeof(CancellationToken)), Snippet.DefaultOf(typeof(CancellationToken))); + + private static ParameterProvider? _enumerationCancellationTokenParameter; + public static ParameterProvider EnumeratorCancellationTokenParameter => _enumerationCancellationTokenParameter ??= new("cancellationToken", $"Enumerator cancellation token", typeof(CancellationToken), Snippet.DefaultOf(typeof(CancellationToken))) { Attributes = new[] { new AttributeStatement(typeof(EnumeratorCancellationAttribute)) } }; + + private static ParameterProvider? _response; + public static ParameterProvider Response => _response ??= new("response", $"Response returned from backend service", CodeModelPlugin.Instance.Configuration.ApiTypes.ResponseType); } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/MethodPropertyBody.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/MethodPropertyBody.cs index aae45abf463..851d75871d4 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/MethodPropertyBody.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/MethodPropertyBody.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Statements; namespace Microsoft.Generator.CSharp { - internal record MethodPropertyBody(MethodBodyStatement Getter, MethodBodyStatement? Setter = null, MethodSignatureModifiers SetterModifiers = MethodSignatureModifiers.None) : PropertyBody; + internal record MethodPropertyBody(MethodBodyStatement Getter, MethodBodyStatement? Setter = null, MethodSignatureModifiers SetterModifiers = MethodSignatureModifiers.None) : PropertyBody(HasSetter: Setter != null); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/MethodSignature.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/MethodSignature.cs index 51d3c7b63f1..b3a9dd6feed 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/MethodSignature.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/MethodSignature.cs @@ -6,6 +6,8 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Statements; namespace Microsoft.Generator.CSharp { @@ -24,24 +26,11 @@ namespace Microsoft.Generator.CSharp /// The generic parameter constraints of the method. /// The explicit interface of the method. /// The non-document comment of the method. - /// Indicates if the summary text is raw. - public sealed record MethodSignature(string Name, FormattableString? Summary, FormattableString? Description, MethodSignatureModifiers Modifiers, CSharpType? ReturnType, FormattableString? ReturnDescription, IReadOnlyList Parameters, IReadOnlyList? Attributes = null, IReadOnlyList? GenericArguments = null, IReadOnlyList? GenericParameterConstraints = null, CSharpType? ExplicitInterface = null, string? NonDocumentComment = null, bool IsRawSummaryText = false) - : MethodSignatureBase(Name, Summary, Description, NonDocumentComment, Modifiers, Parameters, Attributes ?? Array.Empty(), IsRawSummaryText: IsRawSummaryText) + public sealed record MethodSignature(string Name, FormattableString? Summary, FormattableString? Description, MethodSignatureModifiers Modifiers, CSharpType? ReturnType, FormattableString? ReturnDescription, IReadOnlyList Parameters, IReadOnlyList? Attributes = null, IReadOnlyList? GenericArguments = null, IReadOnlyList? GenericParameterConstraints = null, CSharpType? ExplicitInterface = null, string? NonDocumentComment = null) + : MethodSignatureBase(Name, Summary, Description, NonDocumentComment, Modifiers, Parameters, Attributes ?? Array.Empty()) { public static IEqualityComparer ParameterAndReturnTypeEqualityComparer = new MethodSignatureParameterAndReturnTypeEqualityComparer(); - /// - /// Returns a new instance of MethodSignature with all required parameters. - /// - public MethodSignature WithParametersRequired() - { - if (Parameters.All(p => p.DefaultValue is null)) - { - return this; - } - return this with { Parameters = Parameters.Select(p => p.ToRequired()).ToList() }; - } - /// /// Gets the C# reference string for the method. /// @@ -63,7 +52,7 @@ public bool Equals(MethodSignature? x, MethodSignature? y) var result = x.Name == x.Name && x.ReturnType == y.ReturnType - && x.Parameters.SequenceEqual(y.Parameters, Parameter.TypeAndNameEqualityComparer); + && x.Parameters.SequenceEqual(y.Parameters); return result; } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/MethodSignatureBase.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/MethodSignatureBase.cs index 6b01913cb74..a78d9e7b8d3 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/MethodSignatureBase.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/MethodSignatureBase.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Statements; namespace Microsoft.Generator.CSharp { @@ -16,8 +18,7 @@ namespace Microsoft.Generator.CSharp /// The modifiers of the method. /// The parameters of the method. /// The attributes of the method. - /// A flag indicating if the summary text is raw. - public abstract record MethodSignatureBase(string Name, FormattableString? Summary, FormattableString? Description, string? NonDocumentComment, MethodSignatureModifiers Modifiers, IReadOnlyList Parameters, IReadOnlyList Attributes, bool IsRawSummaryText = false) + public abstract record MethodSignatureBase(string Name, FormattableString? Summary, FormattableString? Description, string? NonDocumentComment, MethodSignatureModifiers Modifiers, IReadOnlyList Parameters, IReadOnlyList Attributes) { public FormattableString? SummaryText => Summary.IsNullOrEmpty() ? Description : Summary; public FormattableString? DescriptionText => Summary.IsNullOrEmpty() || Description == Summary || Description?.ToString() == Summary?.ToString() ? $"" : Description; diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/MethodSignatureModifiers.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/MethodSignatureModifiers.cs index 9e72e5424f0..d5d72d46447 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/MethodSignatureModifiers.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/MethodSignatureModifiers.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -18,6 +18,9 @@ public enum MethodSignatureModifiers Virtual = 64, Async = 128, New = 256, - Override = 512 + Override = 512, + Operator = 1024, + Explicit = 2048, + Implicit = 4096 } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ModelTypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ModelTypeProvider.cs deleted file mode 100644 index 9783c77921b..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ModelTypeProvider.cs +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Generator.CSharp.Expressions; -using Microsoft.Generator.CSharp.Input; -using static Microsoft.Generator.CSharp.Expressions.Snippets; - -namespace Microsoft.Generator.CSharp -{ - public sealed class ModelTypeProvider : TypeProvider - { - private readonly InputModelType _inputModel; - public override string Name { get; } - public override string Namespace { get; } - public override FormattableString Description { get; } - - private readonly bool _isStruct; - private readonly TypeSignatureModifiers _declarationModifiers; - - /// - /// The serializations providers for the model provider. - /// - public IReadOnlyList SerializationProviders { get; } = Array.Empty(); - - public ModelTypeProvider(InputModelType inputModel, SourceInputModel? sourceInputModel) - : base(sourceInputModel) - { - _inputModel = inputModel; - Name = inputModel.Name.ToCleanName(); - Namespace = GetDefaultModelNamespace(CodeModelPlugin.Instance.Configuration.Namespace); - Description = inputModel.Description != null ? FormattableStringHelpers.FromString(inputModel.Description) : FormattableStringHelpers.Empty; - // TODO -- support generating models as structs. Tracking issue: https://github.com/microsoft/typespec/issues/3453 - _declarationModifiers = TypeSignatureModifiers.Partial | TypeSignatureModifiers.Class; - if (inputModel.Accessibility == "internal") - { - _declarationModifiers |= TypeSignatureModifiers.Internal; - } - - bool isAbstract = inputModel.DiscriminatorPropertyName is not null && inputModel.DiscriminatorValue is null; - if (isAbstract) - { - _declarationModifiers |= TypeSignatureModifiers.Abstract; - } - - if (inputModel.Usage.HasFlag(InputModelTypeUsage.Json)) - { - SerializationProviders = CodeModelPlugin.Instance.GetSerializationTypeProviders(this); - } - - _isStruct = false; // this is only a temporary placeholder because we do not support to generate structs yet. - } - - protected override TypeSignatureModifiers GetDeclarationModifiers() => _declarationModifiers; - - protected override PropertyDeclaration[] BuildProperties() - { - var propertiesCount = _inputModel.Properties.Count; - var propertyDeclarations = new PropertyDeclaration[propertiesCount]; - - for (int i = 0; i < propertiesCount; i++) - { - var property = _inputModel.Properties[i]; - propertyDeclarations[i] = BuildPropertyDeclaration(property); - } - - return propertyDeclarations; - } - - private PropertyDeclaration BuildPropertyDeclaration(InputModelProperty property) - { - var propertyType = CodeModelPlugin.Instance.TypeFactory.CreateCSharpType(property.Type); - var serializationFormat = CodeModelPlugin.Instance.TypeFactory.GetSerializationFormat(property.Type); - var propHasSetter = PropertyHasSetter(propertyType, property); - MethodSignatureModifiers setterModifier = propHasSetter ? MethodSignatureModifiers.Public : MethodSignatureModifiers.None; - - var propertyDeclaration = new PropertyDeclaration( - Description: PropertyDescriptionBuilder.BuildPropertyDescription(property, propertyType, serializationFormat, !propHasSetter), - Modifiers: MethodSignatureModifiers.Public, - Type: propertyType, - Name: property.Name.FirstCharToUpperCase(), - Body: new AutoPropertyBody(propHasSetter, setterModifier, GetPropertyInitializationValue(property, propertyType)) - ); - - return propertyDeclaration; - } - - /// - /// Returns true if the property has a setter. - /// - /// The of the property. - /// The . - private bool PropertyHasSetter(CSharpType type, InputModelProperty prop) - { - if (prop.IsDiscriminator) - { - return true; - } - - if (prop.IsReadOnly) - { - return false; - } - - if (_isStruct) - { - return false; - } - - if (type.IsLiteral && prop.IsRequired) - { - return false; - } - - if (type.IsCollection && !type.IsReadOnlyMemory) - { - return type.IsNullable; - } - - return true; - } - - private ValueExpression? GetPropertyInitializationValue(InputModelProperty property, CSharpType propertyType) - { - if (!property.IsRequired) - return null; - - if (propertyType.IsLiteral) - { - if (!propertyType.IsNullable) - { - return Literal(propertyType.Literal); - } - else - { - return DefaultOf(propertyType); - } - } - - return null; - } - - protected override CSharpMethod[] BuildConstructors() - { - List constructors = new List(); - - var initializationConstructor = BuildInitializationConstructor(); - if (initializationConstructor != null) - { - constructors.Add(initializationConstructor); - } - - var serializationConstructor = BuildSerializationConstructor(); - bool serializationParametersMatchInitialization = initializationConstructor != null && - initializationConstructor.Signature.Parameters.SequenceEqual(serializationConstructor.Signature.Parameters, Parameter.EqualityComparerByType); - - if (!serializationParametersMatchInitialization) - { - constructors.Add(serializationConstructor); - } - - if (initializationConstructor?.Signature.Parameters.Count > 0) - { - var emptyConstructor = BuildEmptyConstructor(); - constructors.Add(emptyConstructor); - } - - return constructors.ToArray(); - } - - private IReadOnlyList BuildConstructorParameters(bool isSerializationConstructor) - { - List constructorParameters = new List(); - - foreach (var property in _inputModel.Properties) - { - CSharpType propertyType = CodeModelPlugin.Instance.TypeFactory.CreateCSharpType(property.Type); - var initializationValue = GetPropertyInitializationValue(property, propertyType); - var parameterValidation = GetParameterValidation(property, propertyType); - - var parameter = new Parameter( - Name: property.Name.ToVariableName(), - Description: FormattableStringHelpers.FromString(property.Description), - Type: propertyType, - DefaultValue: null, - Validation: parameterValidation, - Initializer: null); - - // All properties should be included in the serialization ctor - if (isSerializationConstructor) - { - constructorParameters.Add(parameter with { Validation = ValidationType.None }); - } - else - { - // For classes, only required + not readonly + not initialization value + not discriminator could get into the public ctor - // For structs, all properties must be set in the public ctor - if (_isStruct || (property is { IsRequired: true, IsDiscriminator: false } && initializationValue == null)) - { - if (!property.IsReadOnly) - { - constructorParameters.Add(parameter with { Type = parameter.Type.InputType }); - } - } - } - } - - return constructorParameters; - } - - private static ValidationType GetParameterValidation(InputModelProperty property, CSharpType propertyType) - { - // We do not validate a parameter when it is a value type (struct or int, etc) - if (propertyType.IsValueType) - { - return ValidationType.None; - } - - // or it is readonly - if (property.IsReadOnly) - { - return ValidationType.None; - } - - // or it is optional - if (!property.IsRequired) - { - return ValidationType.None; - } - - // or it is nullable - if (propertyType.IsNullable) - { - return ValidationType.None; - } - - return ValidationType.AssertNotNull; - } - - private CSharpMethod? BuildInitializationConstructor() - { - if (_inputModel.IsUnknownDiscriminatorModel) - { - return null; - } - - var accessibility = DeclarationModifiers.HasFlag(TypeSignatureModifiers.Abstract) - ? MethodSignatureModifiers.Protected - : _inputModel.Usage.HasFlag(InputModelTypeUsage.Input) - ? MethodSignatureModifiers.Public - : MethodSignatureModifiers.Internal; - var constructorParameters = BuildConstructorParameters(false); - - var constructor = new CSharpMethod( - signature: new ConstructorSignature( - Type, - $"Initializes a new instance of {Type:C}", - null, - accessibility, - constructorParameters), - bodyStatements: new MethodBodyStatement[] - { - new ParameterValidationBlock(constructorParameters), - GetPropertyInitializers(constructorParameters) - }, - kind: CSharpMethodKinds.Constructor); - - return constructor; - } - - private CSharpMethod BuildSerializationConstructor() - { - var constructorParameters = BuildConstructorParameters(true); - - return new CSharpMethod( - signature: new ConstructorSignature( - Type, - $"Initializes a new instance of {Type:C}", - null, - MethodSignatureModifiers.Internal, - constructorParameters), - bodyStatements: new MethodBodyStatement[] - { - new ParameterValidationBlock(constructorParameters), - GetPropertyInitializers(constructorParameters) - }, - kind: CSharpMethodKinds.Constructor); - } - - private MethodBodyStatement GetPropertyInitializers(IReadOnlyList parameters) - { - List methodBodyStatements = new(); - - Dictionary parameterMap = parameters.ToDictionary( - parameter => parameter.Name, - parameter => parameter); - - foreach (var property in Properties) - { - ValueExpression? initializationValue = null; - - if (parameterMap.TryGetValue(property.Name.ToVariableName(), out var parameter) || _isStruct) - { - if (parameter != null) - { - initializationValue = new ParameterReference(parameter); - - if (CSharpType.RequiresToList(parameter.Type, property.Type)) - { - initializationValue = parameter.Type.IsNullable ? - Linq.ToList(new NullConditionalExpression(initializationValue)) : - Linq.ToList(initializationValue); - } - } - } - else if (initializationValue == null && property.Type.IsCollection) - { - initializationValue = New.Instance(property.Type.PropertyInitializationType); - } - - if (initializationValue != null) - { - methodBodyStatements.Add(Assign(new MemberExpression(null, property.Name), initializationValue)); - } - } - - return methodBodyStatements; - } - - private CSharpMethod BuildEmptyConstructor() - { - var accessibility = _isStruct ? MethodSignatureModifiers.Public : MethodSignatureModifiers.Internal; - return new CSharpMethod( - signature: new ConstructorSignature(Type, $"Initializes a new instance of {Type:C} for deserialization.", null, accessibility, Array.Empty()), - bodyStatements: new MethodBodyStatement(), - kind: CSharpMethodKinds.Constructor); - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/OutputLibrary.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/OutputLibrary.cs index dbdf115d48d..74702196a05 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/OutputLibrary.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/OutputLibrary.cs @@ -1,57 +1,81 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.Generator.CSharp.Input; +using System; using System.Collections.Generic; +using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Providers; namespace Microsoft.Generator.CSharp { public class OutputLibrary { - private IReadOnlyList? _models; - private IReadOnlyList? _clients; + private IReadOnlyList? _enums; + private IReadOnlyList? _models; + private IReadOnlyList? _clients; public OutputLibrary() { - EnumMappings = new Dictionary(); - ModelMappings = new Dictionary(); - } + EnumMappings = new Dictionary(); + ModelMappings = new Dictionary(); - public IReadOnlyList Models => _models ??= BuildModels(); - public IReadOnlyList Clients => _clients ??= BuildClients(); + _allModels = new(InitializeAllModels); + } - public IDictionary EnumMappings { get; } - public IDictionary ModelMappings { get; } + private readonly Lazy<(EnumProvider[] Enums, ModelProvider[] Models)> _allModels; - public virtual ModelTypeProvider[] BuildModels() + private (EnumProvider[] Enums, ModelProvider[] Models) InitializeAllModels() { var input = CodeModelPlugin.Instance.InputLibrary.InputNamespace; - var modelsCount = input.Models.Count; - ModelTypeProvider[] modelProviders = new ModelTypeProvider[modelsCount]; - - for (int i = 0; i < modelsCount; i++) + var enums = new EnumProvider[input.Enums.Count]; + for (int i = 0; i < enums.Length; i++) { - var model = input.Models[i]; - var typeProvider = new ModelTypeProvider(model, null); + var inputEnum = input.Enums[i]; + var enumType = EnumProvider.Create(inputEnum); + enums[i] = enumType; + EnumMappings.Add(inputEnum, enumType); + } - modelProviders[i] = typeProvider; - ModelMappings.Add(model, typeProvider); + var models = new ModelProvider[input.Models.Count]; + for (int i = 0; i < models.Length; i++) + { + var inputModel = input.Models[i]; + var model = new ModelProvider(inputModel); + models[i] = model; + ModelMappings.Add(inputModel, model); } - return modelProviders; + return (enums, models); + } + + public IReadOnlyList Enums => _enums ??= BuildEnums(); + public IReadOnlyList Models => _models ??= BuildModels(); + public IReadOnlyList Clients => _clients ??= BuildClients(); + + public IDictionary EnumMappings { get; } + public IDictionary ModelMappings { get; } + + public virtual EnumProvider[] BuildEnums() + { + return _allModels.Value.Enums; + } + + public virtual ModelProvider[] BuildModels() + { + return _allModels.Value.Models; } - public virtual ClientTypeProvider[] BuildClients() + public virtual ClientProvider[] BuildClients() { var input = CodeModelPlugin.Instance.InputLibrary.InputNamespace; var clientsCount = input.Clients.Count; - ClientTypeProvider[] clientProviders = new ClientTypeProvider[clientsCount]; + ClientProvider[] clientProviders = new ClientProvider[clientsCount]; for (int i = 0; i < clientsCount; i++) { - clientProviders[i] = new ClientTypeProvider(input.Clients[i], null); + clientProviders[i] = new ClientProvider(input.Clients[i]); } return clientProviders; diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/Parameter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/Parameter.cs deleted file mode 100644 index 0bc7ba03914..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/Parameter.cs +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Generator.CSharp.Expressions; -using Microsoft.Generator.CSharp.Input; - -namespace Microsoft.Generator.CSharp -{ - public sealed record Parameter(string Name, FormattableString? Description, CSharpType Type, ValueExpression? DefaultValue, ValidationType Validation, ValueExpression? Initializer, bool IsApiVersionParameter = false, bool IsEndpoint = false, bool IsResourceIdentifier = false, bool SkipUrlEncoding = false, RequestLocation RequestLocation = RequestLocation.None, SerializationFormat SerializationFormat = SerializationFormat.Default, bool IsPropertyBag = false, bool IsRef = false, bool IsOut = false) - { - internal bool IsRawData { get; init; } - internal static IEqualityComparer TypeAndNameEqualityComparer = new ParameterTypeAndNameEqualityComparer(); - internal CSharpAttribute[] Attributes { get; init; } = Array.Empty(); - internal bool IsOptionalInSignature => DefaultValue != null; - - /// - /// Creates a from an . - /// - /// The to convert. - public static Parameter FromInputParameter(InputParameter inputParameter) - { - // TO-DO: Add additional implementation to properly build the parameter https://github.com/Azure/autorest.csharp/issues/4607 - var csharpType = CodeModelPlugin.Instance.TypeFactory.CreateCSharpType(inputParameter.Type); - FormattableString? description = FormattableStringHelpers.FromString(inputParameter.Description) ?? FormattableStringHelpers.Empty; - var validation = inputParameter.IsRequired ? ValidationType.AssertNotNull : ValidationType.None; - - return new Parameter( - inputParameter.Name, - description, - csharpType, - null, - validation, - null, - IsApiVersionParameter: inputParameter.IsApiVersion, - IsEndpoint: inputParameter.IsEndpoint, - IsResourceIdentifier: inputParameter.IsResourceParameter, - SkipUrlEncoding: inputParameter.SkipUrlEncoding, - RequestLocation: inputParameter.Location, - SerializationFormat: SerializationFormat.Default); - } - - internal Parameter WithRef(bool isRef = true) => IsRef == isRef ? this : this with { IsRef = isRef }; - internal Parameter ToRequired() - { - return this with { DefaultValue = null }; - } - - internal static ValidationType GetValidation(CSharpType type, RequestLocation requestLocation, bool skipUrlEncoding) - { - if (requestLocation is RequestLocation.Uri or RequestLocation.Path or RequestLocation.Body && type.Equals(typeof(string), ignoreNullable: true) && !skipUrlEncoding) - { - return ValidationType.AssertNotNullOrEmpty; - } - - if (!type.IsValueType) - { - return ValidationType.AssertNotNull; - } - - return ValidationType.None; - } - - internal static readonly IEqualityComparer EqualityComparerByType = new ParameterByTypeEqualityComparer(); - - private struct ParameterByTypeEqualityComparer : IEqualityComparer - { - public bool Equals(Parameter? x, Parameter? y) - { - return Equals(x?.Type, y?.Type); - } - - public int GetHashCode([DisallowNull] Parameter obj) => obj.Type.GetHashCode(); - } - - private class ParameterTypeAndNameEqualityComparer : IEqualityComparer - { - public bool Equals(Parameter? x, Parameter? y) - { - if (ReferenceEquals(x, y)) - { - return true; - } - - if (x is null || y is null) - { - return false; - } - - var result = x.Type.AreNamesEqual(y.Type) && x.Name == y.Name; - return result; - } - - public int GetHashCode([DisallowNull] Parameter obj) - { - // remove type as part of the hash code generation as the type might have changes between versions - return HashCode.Combine(obj.Name); - } - } - - // TO-DO: Migrate code from autorest as part of output classes migration : https://github.com/Azure/autorest.csharp/issues/4198 - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ValidationType.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ParameterValidationType.cs similarity index 71% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ValidationType.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ParameterValidationType.cs index aa4ee34dbb0..3a6c291319c 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ValidationType.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ParameterValidationType.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Generator.CSharp @@ -6,7 +6,7 @@ namespace Microsoft.Generator.CSharp /// /// The set of validation types to perform on a parameter. /// - public enum ValidationType + public enum ParameterValidationType { None, AssertNotNull, diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/PropertyBody.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/PropertyBody.cs index 6f7a8639adb..574fa29adf7 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/PropertyBody.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/PropertyBody.cs @@ -1,7 +1,7 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Generator.CSharp { - public abstract record PropertyBody; + public abstract record PropertyBody(bool HasSetter); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/PropertyDeclaration.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/PropertyDeclaration.cs deleted file mode 100644 index e15c43e61c4..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/PropertyDeclaration.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; - -namespace Microsoft.Generator.CSharp -{ - public record PropertyDeclaration(FormattableString? Description, MethodSignatureModifiers Modifiers, CSharpType Type, string Name, PropertyBody Body, IReadOnlyDictionary? Exceptions = null, CSharpType? ExplicitInterface = null); -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/PropertyDescriptionBuilder.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/PropertyDescriptionBuilder.cs index 8b67583d38f..0ee56b02d2f 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/PropertyDescriptionBuilder.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/PropertyDescriptionBuilder.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.Generator.CSharp.Input; using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Generator.CSharp.Input; namespace Microsoft.Generator.CSharp { @@ -19,22 +19,22 @@ internal static class PropertyDescriptionBuilder /// The serialization format of the property. /// Flag used to determine if the property is read-only. /// The formatted property description string. - internal static FormattableString BuildPropertyDescription(InputModelProperty property, CSharpType type, SerializationFormat serializationFormat, bool isPropertyReadOnly) + internal static IReadOnlyList BuildPropertyDescription(InputModelProperty property, CSharpType type, SerializationFormat serializationFormat, bool isPropertyReadOnly) { - FormattableString description; + List description = new List(); + if (string.IsNullOrEmpty(property.Description)) { - description = CreateDefaultPropertyDescription(property.Name, isPropertyReadOnly); + description.Add(CreateDefaultPropertyDescription(property.Name, isPropertyReadOnly)); } else { - description = FormattableStringHelpers.FromString(property.Description); + description.Add(FormattableStringHelpers.FromString(property.Description)); } if (type.ContainsBinaryData) { - FormattableString binaryDataExtraDescription = CreateBinaryDataExtraDescription(type, serializationFormat); - description = $"{description}{binaryDataExtraDescription}"; + description.AddRange(CreateBinaryDataExtraDescription(type, serializationFormat)); } return description; @@ -82,16 +82,16 @@ internal static IReadOnlyList GetUnionTypesDescriptions(IRead /// /// Creates a default property description based on the property name and if it is read only. /// - private static FormattableString CreateDefaultPropertyDescription(string name, bool isReadOnly) + internal static FormattableString CreateDefaultPropertyDescription(string name, bool isReadOnly) { string splitDeclarationName = string.Join(" ", StringExtensions.SplitByCamelCase(name)).ToLower(); if (isReadOnly) { - return $"Gets the {splitDeclarationName}"; + return $"Gets the {splitDeclarationName}."; } else { - return $"Gets or sets the {splitDeclarationName}"; + return $"Gets or sets the {splitDeclarationName}."; } } @@ -102,7 +102,7 @@ private static FormattableString CreateDefaultPropertyDescription(string name, b /// The CSharpType of the property. /// The serialization format of the property. /// The formatted description string for the property. - private static FormattableString CreateBinaryDataExtraDescription(CSharpType type, SerializationFormat serializationFormat) + private static IReadOnlyList CreateBinaryDataExtraDescription(CSharpType type, SerializationFormat serializationFormat) { string typeSpecificDesc; var unionTypes = GetUnionTypes(type); @@ -129,7 +129,7 @@ private static FormattableString CreateBinaryDataExtraDescription(CSharpType typ return ConstructBinaryDataDescription(typeSpecificDesc, serializationFormat, unionTypeDescriptions); } - return FormattableStringHelpers.Empty; + return Array.Empty(); } // recursively get the union types if any. @@ -147,67 +147,84 @@ private static IReadOnlyList GetUnionTypes(CSharpType type) return Array.Empty(); } - private static FormattableString ConstructBinaryDataDescription(string typeSpecificDesc, SerializationFormat serializationFormat, IReadOnlyList unionTypeDescriptions) + private static IReadOnlyList ConstructBinaryDataDescription(string typeSpecificDesc, SerializationFormat serializationFormat, IReadOnlyList unionTypeDescriptions) { - FormattableString unionTypesAdditionalDescription = $""; + List result = new List(); + List unionTypesAdditionalDescription = new List(); if (unionTypeDescriptions.Count > 0) { - unionTypesAdditionalDescription = $"\n\nSupported types:\n\n"; + unionTypesAdditionalDescription.Add($""); + unionTypesAdditionalDescription.Add($"Supported types:"); + unionTypesAdditionalDescription.Add($""); foreach (FormattableString unionTypeDescription in unionTypeDescriptions) { - unionTypesAdditionalDescription = $"{unionTypesAdditionalDescription}\n{unionTypeDescription}\n\n"; + unionTypesAdditionalDescription.Add($""); + unionTypesAdditionalDescription.Add(unionTypeDescription); + unionTypesAdditionalDescription.Add($""); } - unionTypesAdditionalDescription = $"{unionTypesAdditionalDescription}\n"; + unionTypesAdditionalDescription.Add($""); + unionTypesAdditionalDescription.Add($""); } + switch (serializationFormat) { case SerializationFormat.Bytes_Base64Url: case SerializationFormat.Bytes_Base64: - return $@" - -To assign a byte[] to {typeSpecificDesc} use . -The byte[] will be serialized to a Base64 encoded string. - -{unionTypesAdditionalDescription} -Examples: - - -BinaryData.FromBytes(new byte[] {{ 1, 2, 3 }}) -Creates a payload of ""AQID"". - - -"; + result.Add($""); + result.Add($"To assign a byte[] to {typeSpecificDesc} use ."); + result.Add($"The byte[] will be serialized to a Base64 encoded string."); + result.Add($""); + result.Add($""); + if (unionTypesAdditionalDescription.Count > 0) + { + result.AddRange(unionTypesAdditionalDescription); + } + result.Add($"Examples:"); + result.Add($""); + result.Add($""); + result.Add($"BinaryData.FromBytes(new byte[] {{ 1, 2, 3 }})"); + result.Add($"Creates a payload of \"AQID\"."); + result.Add($""); + result.Add($""); + result.Add($""); + break; default: - return $@" - -To assign an object to {typeSpecificDesc} use . - - -To assign an already formatted json string to this property use . - -{unionTypesAdditionalDescription} -Examples: - - -BinaryData.FromObjectAsJson(""foo"") -Creates a payload of ""foo"". - - -BinaryData.FromString(""\""foo\"""") -Creates a payload of ""foo"". - - -BinaryData.FromObjectAsJson(new {{ key = ""value"" }}) -Creates a payload of {{ ""key"": ""value"" }}. - - -BinaryData.FromString(""{{\""key\"": \""value\""}}"") -Creates a payload of {{ ""key"": ""value"" }}. - - -"; + result.Add($""); + result.Add($"To assign an object to {typeSpecificDesc} use ."); + result.Add($""); + result.Add($""); + result.Add($"To assign an already formatted json string to this property use ."); + result.Add($""); + result.Add($""); + if (unionTypesAdditionalDescription.Count > 0) + { + result.AddRange(unionTypesAdditionalDescription); + } + result.Add($"Examples:"); + result.Add($""); + result.Add($""); + result.Add($"BinaryData.FromObjectAsJson(\"foo\")"); + result.Add($"Creates a payload of \"foo\"."); + result.Add($""); + result.Add($""); + result.Add($"BinaryData.FromString(\"\\\"foo\\\"\")"); + result.Add($"Creates a payload of \"foo\"."); + result.Add($""); + result.Add($""); + result.Add($"BinaryData.FromObjectAsJson(new {{ key = \"value\" }})"); + result.Add($"Creates a payload of {{ \"key\": \"value\" }}."); + result.Add($""); + result.Add($""); + result.Add($"BinaryData.FromString(\"{{\\\"key\\\": \\\"value\\\"}}\")"); + result.Add($"Creates a payload of {{ \"key\": \"value\" }}."); + result.Add($""); + result.Add($""); + result.Add($""); + break; } + + return result; } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/TypeFactory.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/TypeFactory.cs index 29df093f8e1..03195f21069 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/TypeFactory.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/TypeFactory.cs @@ -1,7 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; +using System.Collections.Generic; using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Providers; namespace Microsoft.Generator.CSharp { @@ -15,11 +18,11 @@ public abstract class TypeFactory public abstract CSharpType CreateCSharpType(InputType input); /// - /// Factory method for creating a based on an input parameter . + /// Factory method for creating a based on an input parameter . /// /// The to convert. - /// An instance of . - public abstract Parameter CreateCSharpParam(InputParameter parameter); + /// An instance of . + public abstract ParameterProvider CreateCSharpParam(InputParameter parameter); /// /// Factory method for creating a based on an input operation . @@ -37,25 +40,39 @@ public abstract class TypeFactory /// The for the input type. public SerializationFormat GetSerializationFormat(InputType input) => input switch { - InputLiteralType literalType => GetSerializationFormat(literalType.LiteralValueType), - InputList listType => GetSerializationFormat(listType.ElementType), - InputDictionary dictionaryType => GetSerializationFormat(dictionaryType.ValueType), + InputLiteralType literalType => GetSerializationFormat(literalType.ValueType), + InputListType listType => GetSerializationFormat(listType.ElementType), + InputDictionaryType dictionaryType => GetSerializationFormat(dictionaryType.ValueType), + InputDateTimeType dateTimeType => dateTimeType.Encode switch + { + DateTimeKnownEncoding.Rfc3339 => SerializationFormat.DateTime_RFC3339, + DateTimeKnownEncoding.Rfc7231 => SerializationFormat.DateTime_RFC7231, + DateTimeKnownEncoding.UnixTimestamp => SerializationFormat.DateTime_Unix, + _ => throw new IndexOutOfRangeException($"unknown encode {dateTimeType.Encode}"), + }, + InputDurationType durationType => durationType.Encode switch + { + // there is no such thing as `DurationConstant` + DurationKnownEncoding.Iso8601 => SerializationFormat.Duration_ISO8601, + DurationKnownEncoding.Seconds => durationType.WireType.Kind switch + { + InputPrimitiveTypeKind.Int32 => SerializationFormat.Duration_Seconds, + InputPrimitiveTypeKind.Float or InputPrimitiveTypeKind.Float32 => SerializationFormat.Duration_Seconds_Float, + _ => SerializationFormat.Duration_Seconds_Double + }, + DurationKnownEncoding.Constant => SerializationFormat.Duration_Constant, + _ => throw new IndexOutOfRangeException($"unknown encode {durationType.Encode}") + }, InputPrimitiveType primitiveType => primitiveType.Kind switch { - InputPrimitiveTypeKind.BytesBase64Url => SerializationFormat.Bytes_Base64Url, - InputPrimitiveTypeKind.Bytes => SerializationFormat.Bytes_Base64, - InputPrimitiveTypeKind.Date => SerializationFormat.Date_ISO8601, - InputPrimitiveTypeKind.DateTime => SerializationFormat.DateTime_ISO8601, - InputPrimitiveTypeKind.DateTimeISO8601 => SerializationFormat.DateTime_ISO8601, - InputPrimitiveTypeKind.DateTimeRFC1123 => SerializationFormat.DateTime_RFC1123, - InputPrimitiveTypeKind.DateTimeRFC3339 => SerializationFormat.DateTime_RFC3339, - InputPrimitiveTypeKind.DateTimeRFC7231 => SerializationFormat.DateTime_RFC7231, - InputPrimitiveTypeKind.DateTimeUnix => SerializationFormat.DateTime_Unix, - InputPrimitiveTypeKind.DurationISO8601 => SerializationFormat.Duration_ISO8601, - InputPrimitiveTypeKind.DurationConstant => SerializationFormat.Duration_Constant, - InputPrimitiveTypeKind.DurationSeconds => SerializationFormat.Duration_Seconds, - InputPrimitiveTypeKind.DurationSecondsFloat => SerializationFormat.Duration_Seconds_Float, - InputPrimitiveTypeKind.Time => SerializationFormat.Time_ISO8601, + InputPrimitiveTypeKind.PlainDate => SerializationFormat.Date_ISO8601, + InputPrimitiveTypeKind.PlainTime => SerializationFormat.Time_ISO8601, + InputPrimitiveTypeKind.Bytes => primitiveType.Encode switch + { + BytesKnownEncoding.Base64 => SerializationFormat.Bytes_Base64, + BytesKnownEncoding.Base64Url => SerializationFormat.Bytes_Base64Url, + _ => throw new IndexOutOfRangeException($"unknown encode {primitiveType.Encode}") + }, _ => SerializationFormat.Default }, _ => SerializationFormat.Default @@ -65,5 +82,15 @@ public abstract class TypeFactory public abstract CSharpType RequestConditionsType(); public abstract CSharpType TokenCredentialType(); public abstract CSharpType PageResponseType(); + + /// + /// The initialization type of list properties. This type should implement both and . + /// + public virtual CSharpType ListInitializationType => ChangeTrackingListProvider.Instance.Type; + + /// + /// The initialization type of dictionary properties. This type should implement both and . + /// + public virtual CSharpType DictionaryInitializationType => ChangeTrackingDictionaryProvider.Instance.Type; } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/TypeSignatureModifiers.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/TypeSignatureModifiers.cs index 7bc6c816edc..eb559105c5b 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/TypeSignatureModifiers.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/TypeSignatureModifiers.cs @@ -24,5 +24,6 @@ public enum TypeSignatureModifiers Partial = 1 << 9, Sealed = 1 << 10, Abstract = 1 << 11, + ReadOnly = 1 << 12, } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/DiagnosticAttribute.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/PostProcessing/CodeFile.cs similarity index 50% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/DiagnosticAttribute.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/PostProcessing/CodeFile.cs index 737717cac47..7fe23b0fb18 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/DiagnosticAttribute.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/PostProcessing/CodeFile.cs @@ -1,19 +1,18 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.Generator.CSharp.Expressions; - namespace Microsoft.Generator.CSharp { - public class DiagnosticAttribute + public class CodeFile { - public DiagnosticAttribute(string name, ValueExpression value) + internal CodeFile(string content, string name) { Name = name; - Value = value; + Content = content; } public string Name { get; } - public ValueExpression Value { get; } + + public string Content { get; } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/PostProcessing/GeneratedCodeWorkspace.cs index 83f012e1e45..10e8eca0156 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -24,7 +24,6 @@ internal class GeneratedCodeWorkspace private static readonly Lazy> _assemblyMetadataReferences = new(() => new List() { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) }); private static readonly Lazy _metadataReferenceResolver = new(() => new WorkspaceMetadataReferenceResolver()); - private static readonly CSharpSyntaxRewriter SA1505Rewriter = new SA1505Rewriter(); private static Task? _cachedProject; private static readonly string[] _generatedFolders = { GeneratedFolder }; private static readonly string _newLine = "\n"; @@ -52,9 +51,9 @@ public void AddPlainFiles(string name, string content) PlainFiles.Add(name, content); } - public async Task AddGeneratedFile(string name, string text) + public async Task AddGeneratedFile(CodeFile codefile) { - var document = _project.AddDocument(name, text, _generatedFolders); + var document = _project.AddDocument(codefile.Name, codefile.Content, _generatedFolders); var root = await document.GetSyntaxRootAsync(); Debug.Assert(root != null); @@ -93,15 +92,7 @@ public async Task AddGeneratedFile(string name, string text) private async Task ProcessDocument(Document document) { - var syntaxTree = await document.GetSyntaxTreeAsync(); - if (syntaxTree != null) - { - var root = await syntaxTree.GetRootAsync(); - document = document.WithSyntaxRoot(SA1505Rewriter.Visit(root)); - } - document = await Simplifier.ReduceAsync(document); - document = await Formatter.FormatAsync(document); return document; } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/PostProcessing/SA1505Rewriter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/PostProcessing/SA1505Rewriter.cs deleted file mode 100644 index b51de9391e7..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/PostProcessing/SA1505Rewriter.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace Microsoft.Generator.CSharp -{ - /// - /// This Roslyn CSharpSyntaxRewriter will delete extra newlines after the - /// open brace of a class/struct/enum definition. It is for resolving SA1505 error. - /// - internal class SA1505Rewriter : CSharpSyntaxRewriter - { - public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node) - => base.VisitClassDeclaration((ClassDeclarationSyntax)RemoveEOLAfterOpenBrace(node)); - - public override SyntaxNode? VisitStructDeclaration(StructDeclarationSyntax node) - => base.VisitStructDeclaration((StructDeclarationSyntax)RemoveEOLAfterOpenBrace(node)); - - public override SyntaxNode? VisitEnumDeclaration(EnumDeclarationSyntax node) - => base.VisitEnumDeclaration((EnumDeclarationSyntax)RemoveEOLAfterOpenBrace(node)); - - private BaseTypeDeclarationSyntax RemoveEOLAfterOpenBrace(BaseTypeDeclarationSyntax node) - { - // all extra EOL after open brace are the leading trivia of the next token - var nextToken = node.OpenBraceToken.GetNextToken(); - if (nextToken.IsMissing) - { - return node; - } - - var leadingTrivia = nextToken.LeadingTrivia; - if (leadingTrivia.Count == 0 || !leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia)) - { - return node; - } - - var newLeadingTrivia = leadingTrivia.SkipWhile(t => t.IsKind(SyntaxKind.EndOfLineTrivia)); - var newNextToken = nextToken.WithLeadingTrivia(newLeadingTrivia); - return node.ReplaceToken(nextToken, newNextToken); - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Properties/AssemblyInfo.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Properties/AssemblyInfo.cs index ee3066513e8..d1db48955a3 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Properties/AssemblyInfo.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Properties/AssemblyInfo.cs @@ -1,7 +1,8 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.Generator.CSharp.Tests")] +[assembly: InternalsVisibleTo("Microsoft.Generator.CSharp.Tests.Perf")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ArgumentProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ArgumentProvider.cs new file mode 100644 index 00000000000..659e111fca3 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ArgumentProvider.cs @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Snippets; +using Microsoft.Generator.CSharp.Statements; +using static Microsoft.Generator.CSharp.Snippets.Snippet; + +namespace Microsoft.Generator.CSharp.Providers +{ + internal class ArgumentProvider : TypeProvider + { + private static readonly Lazy _instance = new(() => new ArgumentProvider()); + + private class Template { } + + private const string AssertNotNullMethodName = "AssertNotNull"; + private const string AssertNotNullOrEmptyMethodName = "AssertNotNullOrEmpty"; + private const string AssertNotNullOrWhiteSpaceMethodName = "AssertNotNullOrWhiteSpace"; + + private readonly CSharpType _t = typeof(Template<>).GetGenericArguments()[0]; + private readonly ParameterProvider _nameParam = new ParameterProvider("name", $"The name.", typeof(string)); + private readonly CSharpType _nullableT; + private readonly ParameterReferenceSnippet _nameParamRef; + + public static ArgumentProvider Instance => _instance.Value; + + protected override string GetFileName() => Path.Combine("src", "Generated", "Internal", $"{Name}.cs"); + + private ArgumentProvider() + { + _nameParamRef = new ParameterReferenceSnippet(_nameParam); + _nullableT = _t.WithNullable(true); + } + + protected override TypeSignatureModifiers GetDeclarationModifiers() + { + return TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static; + } + + public override string Name => "Argument"; + + private MethodSignature GetSignature( + string name, + IReadOnlyList parameters, + IReadOnlyList? genericArguments = null, + IReadOnlyList? whereExpressions = null, + CSharpType? returnType = null) + { + return new MethodSignature( + name, + null, + null, + MethodSignatureModifiers.Static | MethodSignatureModifiers.Public, + returnType, + null, + parameters, + GenericArguments: genericArguments, + GenericParameterConstraints: whereExpressions); + } + + protected override MethodProvider[] BuildMethods() + { + return + [ + BuildAssertNotNull(), + BuildAssertNotNullStruct(), + BuildAssertNotNullOrEmptyCollection(), + BuildAssertNotNullOrEmptyString(), + BuildAssertNotNullOrWhiteSpace(), + BuildAssertNotDefault(), + BuildAssertInRange(), + BuildAssertEnumDefined(), + BuildCheckNotNull(), + BuildCheckNotNullOrEmptyString(), + BuildAssertNull(), + ]; + } + + + private MethodProvider BuildAssertNull() + { + var valueParam = new ParameterProvider("value", $"The value.", _t); + var messageParam = new ParameterProvider("message", $"The message.", typeof(string), DefaultOf(new CSharpType(typeof(string), true))); + var signature = GetSignature("AssertNull", [valueParam, _nameParam, messageParam], [_t]); + var value = new ParameterReferenceSnippet(valueParam); + var message = new ParameterReferenceSnippet(messageParam); + return new MethodProvider(signature, new MethodBodyStatement[] + { + new IfStatement(NotEqual(value, Null)) + { + ThrowArgumentException(NullCoalescing(message, Literal("Value must be null."))) + } + }); + } + + private MethodProvider BuildCheckNotNullOrEmptyString() + { + var valueParam = new ParameterProvider("value", $"The value.", typeof(string)); + var signature = GetSignature("CheckNotNullOrEmpty", [valueParam, _nameParam], returnType: typeof(string)); + var value = new ParameterReferenceSnippet(valueParam); + return new MethodProvider(signature, new MethodBodyStatement[] + { + AssertNotNullOrEmpty(value, _nameParamRef), + Return(value) + }); + } + + private MethodProvider BuildCheckNotNull() + { + var valueParam = new ParameterProvider("value", $"The value.", _t); + var signature = GetSignature("CheckNotNull", [valueParam, _nameParam], new[] { _t }, new[] { Where.Class(_t) }, _t); + var value = new ParameterReferenceSnippet(valueParam); + return new MethodProvider(signature, new MethodBodyStatement[] + { + AssertNotNull(value, _nameParamRef), + Return(value) + }); + } + + private MethodProvider BuildAssertEnumDefined() + { + var valueParam = new ParameterProvider("value", $"The value.", typeof(object), null); + var enumTypeParam = new ParameterProvider("enumType", $"The enum value.", typeof(Type)); + var signature = GetSignature("AssertEnumDefined", [enumTypeParam, valueParam, _nameParam]); + var enumType = new ParameterReferenceSnippet(enumTypeParam).Untyped; + var value = new ParameterReferenceSnippet(valueParam).Untyped; + return new MethodProvider(signature, new MethodBodyStatement[] + { + new IfStatement(Not(new BoolSnippet(new InvokeStaticMethodExpression(typeof(Enum), "IsDefined", [enumType, value])))) + { + ThrowArgumentException(new FormattableStringExpression("Value not defined for {0}.", [new MemberExpression(enumType, "FullName")])) + } + }); + } + + private MethodProvider BuildAssertInRange() + { + var valueParam = new ParameterProvider("value", $"The value.", _t); + var minParam = new ParameterProvider("minimum", $"The minimum value.", _t); + var maxParam = new ParameterProvider("maximum", $"The maximum value.", _t); + var whereExpressions = new WhereExpression[] { Where.NotNull(_t).And(new CSharpType(typeof(IComparable<>), _t)) }; + var signature = GetSignature("AssertInRange", new[] { valueParam, minParam, maxParam, _nameParam }, new[] { _t }, whereExpressions); + var value = new ParameterReferenceSnippet(valueParam); + return new MethodProvider(signature, new MethodBodyStatement[] + { + new IfStatement(GreaterThan(GetCompareToExpression(new ParameterReferenceSnippet(minParam), value), Literal(0))) + { + Throw(New.ArgumentOutOfRangeException(_nameParamRef, "Value is less than the minimum allowed.", false)) + }, + new IfStatement(LessThan(GetCompareToExpression(new ParameterReferenceSnippet(maxParam), value), Literal(0))) + { + Throw(New.ArgumentOutOfRangeException(_nameParamRef, "Value is greater than the maximum allowed.", false)) + } + }); + } + + private ValueExpression GetCompareToExpression(ValueExpression left, ValueExpression right) + { + return left.Invoke("CompareTo", right); + } + + private MethodProvider BuildAssertNotDefault() + { + var valueParam = new ParameterProvider("value", $"The value.", _t); + var valueParamWithRef = new ParameterProvider("value", $"The value.", _t, null, true); + var whereExpressions = new WhereExpression[] { Where.Struct(_t).And(new CSharpType(typeof(IEquatable<>), _t)) }; + var signature = GetSignature("AssertNotDefault", [valueParamWithRef, _nameParam], [_t], whereExpressions); + var value = new ParameterReferenceSnippet(valueParam); + return new MethodProvider(signature, new MethodBodyStatement[] + { + new IfStatement(new BoolSnippet(value.Untyped.Invoke("Equals", Default))) + { + ThrowArgumentException("Value cannot be empty.") + } + }); + } + + private MethodProvider BuildAssertNotNullOrWhiteSpace() + { + var valueParam = new ParameterProvider("value", $"The value.", typeof(string)); + var signature = GetSignature(AssertNotNullOrWhiteSpaceMethodName, [valueParam, _nameParam]); + var value = new StringSnippet(valueParam); + return new MethodProvider(signature, new MethodBodyStatement[] + { + AssertNotNullSnippet(valueParam), + new IfStatement(StringSnippet.IsNullOrWhiteSpace(value)) + { + ThrowArgumentException("Value cannot be empty or contain only white-space characters.") + } + }); + } + + private MethodProvider BuildAssertNotNullOrEmptyString() + { + var valueParam = new ParameterProvider("value", $"The value.", typeof(string)); + var signature = GetSignature(AssertNotNullOrEmptyMethodName, [valueParam, _nameParam]); + var value = new StringSnippet(valueParam); + return new MethodProvider(signature, new MethodBodyStatement[] + { + AssertNotNullSnippet(valueParam), + new IfStatement(Equal(value.Length, Literal(0))) + { + ThrowArgumentException("Value cannot be an empty string.") + } + }); + } + + private MethodProvider BuildAssertNotNullOrEmptyCollection() + { + const string throwMessage = "Value cannot be an empty collection."; + var valueParam = new ParameterProvider("value", $"The value.", new CSharpType(typeof(IEnumerable<>), _t)); + var signature = GetSignature(AssertNotNullOrEmptyMethodName, [valueParam, _nameParam], [_t]); + return new MethodProvider(signature, new MethodBodyStatement[] + { + AssertNotNullSnippet(valueParam), + new IfStatement(IsCollectionEmpty(valueParam, new VariableReferenceSnippet(new CSharpType(typeof(ICollection<>), _t), new CodeWriterDeclaration("collectionOfT")))) + { + ThrowArgumentException(throwMessage) + }, + new IfStatement(IsCollectionEmpty(valueParam, new VariableReferenceSnippet(typeof(ICollection), new CodeWriterDeclaration("collection")))) + { + ThrowArgumentException(throwMessage) + }, + UsingDeclare("e", new CSharpType(typeof(IEnumerator<>), _t), new ParameterReferenceSnippet(valueParam).Untyped.Invoke("GetEnumerator"), out var eVar), + new IfStatement(Not(new BoolSnippet(eVar.Untyped.Invoke("MoveNext")))) + { + ThrowArgumentException(throwMessage) + } + }); + } + + private static BoolSnippet IsCollectionEmpty(ParameterProvider valueParam, VariableReferenceSnippet collection) + { + return BoolSnippet.Is(valueParam, new DeclarationExpression(collection.Type, collection.Declaration, false)).And(Equal(new MemberExpression(collection, "Count"), Literal(0))); + } + + private MethodBodyStatement ThrowArgumentException(ValueExpression expression) + { + return Throw(New.ArgumentException(_nameParamRef, expression, false)); + } + + private MethodBodyStatement ThrowArgumentException(string message) => ThrowArgumentException(Literal(message)); + + private MethodProvider BuildAssertNotNullStruct() + { + var valueParam = new ParameterProvider("value", $"The value.", _nullableT); + var signature = GetSignature(AssertNotNullMethodName, [valueParam, _nameParam], [_t], [Where.Struct(_t)]); + var value = new ParameterReferenceSnippet(valueParam); + return new MethodProvider(signature, new MethodBodyStatement[] + { + new IfStatement(Not(new BoolSnippet(new MemberExpression(value, "HasValue")))) + { + Throw(New.ArgumentNullException(_nameParamRef, false)) + } + }); + } + + private MethodProvider BuildAssertNotNull() + { + var valueParam = new ParameterProvider("value", $"The value.", _t); + var signature = GetSignature(AssertNotNullMethodName, [valueParam, _nameParam], [_t]); + return new MethodProvider(signature, new MethodBodyStatement[] + { + AssertNotNullSnippet(valueParam) + }); + } + + private IfStatement AssertNotNullSnippet(ParameterProvider valueParam) + { + return new IfStatement(Is(new ParameterReferenceSnippet(valueParam), Null)) + { + Throw(New.ArgumentNullException(_nameParamRef, false)) + }; + } + + internal MethodBodyStatement AssertNotNull(ValueExpression variable, ValueExpression? name = null) + { + return new InvokeStaticMethodStatement(Type, AssertNotNullMethodName, variable, name ?? Nameof(variable)); + } + + internal MethodBodyStatement AssertNotNullOrEmpty(ValueExpression variable, ValueExpression? name = null) + { + return new InvokeStaticMethodStatement(Type, AssertNotNullOrEmptyMethodName, variable, name ?? Nameof(variable)); + } + + internal MethodBodyStatement AssertNotNullOrWhiteSpace(ValueExpression variable, ValueExpression? name = null) + { + return new InvokeStaticMethodStatement(Type, AssertNotNullOrWhiteSpaceMethodName, variable, name ?? Nameof(variable)); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ChangeTrackingDictionaryProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ChangeTrackingDictionaryProvider.cs new file mode 100644 index 00000000000..99fd63cc3af --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ChangeTrackingDictionaryProvider.cs @@ -0,0 +1,402 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Snippets; +using Microsoft.Generator.CSharp.Statements; +using static Microsoft.Generator.CSharp.Snippets.Snippet; + +namespace Microsoft.Generator.CSharp.Providers +{ + internal sealed class ChangeTrackingDictionaryProvider : TypeProvider + { + private static readonly Lazy _instance = new(() => new ChangeTrackingDictionaryProvider()); + public static ChangeTrackingDictionaryProvider Instance => _instance.Value; + + private class ChangeTrackingDictionaryTemplate { } + private readonly CSharpType _tKey = typeof(ChangeTrackingDictionaryTemplate<,>).GetGenericArguments()[0]; + private readonly CSharpType _tValue = typeof(ChangeTrackingDictionaryTemplate<,>).GetGenericArguments()[1]; + + private readonly ParameterProvider _indexParam; + private readonly CSharpType _dictionary; + private readonly CSharpType _IDictionary; + private readonly CSharpType _IReadOnlyDictionary; + private readonly CSharpType _IEnumerator; + private readonly CSharpType _keyValuePair; + private readonly FieldProvider _innerDictionaryField; + private readonly DictionarySnippet _innerDictionary; + private readonly MethodSignature _ensureDictionarySignature; + + private InvokeInstanceMethodExpression EnsureDictionary { get; init; } + private BoolSnippet IsUndefined { get; } = new BoolSnippet(new MemberExpression(This, "IsUndefined")); + + protected override string GetFileName() => Path.Combine("src", "Generated", "Internal", $"{Name}.cs"); + + private ChangeTrackingDictionaryProvider() + { + WhereClause = Where.NotNull(_tKey); + _indexParam = new ParameterProvider("key", $"The key.", _tKey); + _IDictionary = new CSharpType(typeof(IDictionary<,>), _tKey, _tValue); + _dictionary = new CSharpType(typeof(Dictionary<,>), _tKey, _tValue); + _IReadOnlyDictionary = new CSharpType(typeof(IReadOnlyDictionary<,>), _tKey, _tValue); + _IEnumerator = new CSharpType(typeof(IEnumerator<>), new CSharpType(typeof(KeyValuePair<,>), _tKey, _tValue)); + _keyValuePair = new CSharpType(typeof(KeyValuePair<,>), _tKey, _tValue); + _innerDictionaryField = new FieldProvider(FieldModifiers.Private, new CSharpType(typeof(IDictionary<,>), _tKey, _tValue), "_innerDictionary"); + _innerDictionary = new DictionarySnippet(_tKey, _tValue, new VariableReferenceSnippet(_IDictionary, _innerDictionaryField.Declaration)); + _ensureDictionarySignature = new MethodSignature("EnsureDictionary", null, null, MethodSignatureModifiers.Public, _IDictionary, null, Array.Empty()); + EnsureDictionary = This.Invoke(_ensureDictionarySignature); + } + + protected override TypeSignatureModifiers GetDeclarationModifiers() + { + return TypeSignatureModifiers.Internal; + } + + public override string Name => "ChangeTrackingDictionary"; + + protected override CSharpType[] BuildTypeArguments() + { + return new[] { _tKey, _tValue }; + } + + protected override FieldProvider[] BuildFields() + { + return new[] { _innerDictionaryField }; + } + + protected override CSharpType[] BuildImplements() + { + return new[] { _IDictionary, _IReadOnlyDictionary }; + } + + protected override MethodProvider[] BuildConstructors() + { + return new MethodProvider[] + { + DefaultConstructor(), + ConstructorWithDictionary(), + ConstructorWithReadOnlyDictionary() + }; + } + + private MethodProvider ConstructorWithReadOnlyDictionary() + { + var dictionaryParam = new ParameterProvider("dictionary", $"The inner dictionary.", _IReadOnlyDictionary); + var dictionary = new DictionarySnippet(_tKey, _tValue, dictionaryParam); + var signature = new ConstructorSignature(Type, null, null, MethodSignatureModifiers.Public, new[] { dictionaryParam }); + return new MethodProvider(signature, new MethodBodyStatement[] + { + new IfStatement(Equal(dictionary, Null)) + { + Return() + }, + Assign(_innerDictionary, New.Instance(_dictionary)), + new ForeachStatement("pair", dictionary, out var pair) + { + _innerDictionary.Add(pair) + } + }); + } + + private MethodProvider ConstructorWithDictionary() + { + var dictionaryParam = new ParameterProvider("dictionary", $"The inner dictionary.", _IDictionary); + var dictionary = new ParameterReferenceSnippet(dictionaryParam); + var signature = new ConstructorSignature(Type, null, null, MethodSignatureModifiers.Public, new[] { dictionaryParam }); + return new MethodProvider(signature, new MethodBodyStatement[] + { + new IfStatement(Equal(dictionary, Null)) + { + Return() + }, + Assign(_innerDictionary, New.Instance(_dictionary, dictionary)) + }); + } + + private MethodProvider DefaultConstructor() + { + var signature = new ConstructorSignature(Type, null, null, MethodSignatureModifiers.Public, Array.Empty()); + return new MethodProvider(signature, Array.Empty()); + } + + protected override PropertyProvider[] BuildProperties() + { + return new PropertyProvider[] + { + BuildIsUndefined(), + BuildCount(), + BuildIsReadOnly(), + BuildKeys(), + BuildValues(), + BuildIndexer(), + BuildEnumerableKeys(), + BuildEnumerableValues() + }; + } + + private PropertyProvider BuildEnumerableValues() + { + return new PropertyProvider(null, MethodSignatureModifiers.None, new CSharpType(typeof(IEnumerable<>), _tValue), "Values", new ExpressionPropertyBody( + new MemberExpression(This, "Values")), + null, + _IReadOnlyDictionary); + } + + private PropertyProvider BuildEnumerableKeys() + { + return new PropertyProvider(null, MethodSignatureModifiers.None, new CSharpType(typeof(IEnumerable<>), _tKey), "Keys", new ExpressionPropertyBody( + new MemberExpression(This, "Keys")), + null, + _IReadOnlyDictionary); + } + + private PropertyProvider BuildIndexer() + { + var indexParam = new ParameterProvider("key", $"The key.", _tKey); + return new IndexerProvider(null, MethodSignatureModifiers.Public, _tValue, indexParam, new MethodPropertyBody( + new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Throw(New.Instance(typeof(KeyNotFoundException), Nameof(new ParameterReferenceSnippet(_indexParam)))) + }, + Return(new ArrayElementExpression(EnsureDictionary, new ParameterReferenceSnippet(_indexParam))), + }, + new MethodBodyStatement[] + { + Assign( + new ArrayElementExpression(EnsureDictionary, new ParameterReferenceSnippet(_indexParam)), + new KeywordExpression("value", null)) + })); + } + + private PropertyProvider BuildValues() + { + return new PropertyProvider(null, MethodSignatureModifiers.Public, new CSharpType(typeof(ICollection<>), _tValue), "Values", + new ExpressionPropertyBody(new TernaryConditionalExpression( + IsUndefined, + new InvokeStaticMethodExpression(typeof(Array), "Empty", Array.Empty(), new[] { _tValue }), + new MemberExpression(EnsureDictionary, "Values")))); + } + + private PropertyProvider BuildKeys() + { + return new PropertyProvider(null, MethodSignatureModifiers.Public, new CSharpType(typeof(ICollection<>), _tKey), "Keys", + new ExpressionPropertyBody(new TernaryConditionalExpression( + IsUndefined, + new InvokeStaticMethodExpression(typeof(Array), "Empty", Array.Empty(), new[] { _tKey }), + new MemberExpression(EnsureDictionary, "Keys")))); + } + + private PropertyProvider BuildIsReadOnly() + { + return new PropertyProvider($"Gets the IsReadOnly", MethodSignatureModifiers.Public, typeof(bool), "IsReadOnly", + new ExpressionPropertyBody(new TernaryConditionalExpression( + IsUndefined, + False, + new MemberExpression(EnsureDictionary, "IsReadOnly")))); + } + + private PropertyProvider BuildCount() + { + return new PropertyProvider(null, MethodSignatureModifiers.Public, typeof(int), "Count", + new ExpressionPropertyBody(new TernaryConditionalExpression( + IsUndefined, + Literal(0), + new MemberExpression(EnsureDictionary, "Count")))); + } + + private PropertyProvider BuildIsUndefined() + { + return new PropertyProvider(null, MethodSignatureModifiers.Public, typeof(bool), "IsUndefined", new ExpressionPropertyBody(Equal(_innerDictionary, Null))); + } + + private MethodSignature GetSignature( + string name, + CSharpType? returnType, + MethodSignatureModifiers modifiers = MethodSignatureModifiers.Public, + IReadOnlyList? parameters = null, + CSharpType? explicitImpl = null) + { + return new MethodSignature(name, null, null, modifiers, returnType, null, parameters ?? Array.Empty(), ExplicitInterface: explicitImpl); + } + + protected override MethodProvider[] BuildMethods() + { + return new MethodProvider[] + { + BuildGetEnumeratorGeneric(), + BuildGetEnumerator(), + BuildAddPair(), + BuildClear(), + BuildContains(), + BuildCopyTo(), + BuildRemovePair(), + BuildAdd(), + BuildContainsKey(), + BuildRemoveKey(), + BuildTryGetValue(), + BuildEnsureDictionary() + }; + } + + private MethodProvider BuildTryGetValue() + { + var keyParam = new ParameterProvider("key", $"The key to search for.", _tKey); + var valueParam = new ParameterProvider("value", $"The value.", _tValue, isOut: true); + var value = new ParameterReferenceSnippet(valueParam); + var signature = GetSignature("TryGetValue", typeof(bool), parameters: new[] { keyParam, valueParam }); + return new MethodProvider(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Assign(value, Default), + Return(False) + }, + Return(EnsureDictionary.Invoke("TryGetValue", new ParameterReferenceSnippet(keyParam), new KeywordExpression("out", value))) + }); + } + + private MethodProvider BuildRemoveKey() + { + var keyParam = new ParameterProvider("key", $"The key.", _tKey); + var signature = GetSignature("Remove", typeof(bool), parameters: new[] { keyParam }); + return new MethodProvider(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Return(False) + }, + Return(EnsureDictionary.Invoke("Remove", new ParameterReferenceSnippet(keyParam))) + }); + } + + private MethodProvider BuildContainsKey() + { + var keyParam = new ParameterProvider("key", $"The key to search for.", _tKey); + var signature = GetSignature("ContainsKey", typeof(bool), parameters: new[] { keyParam }); + return new MethodProvider(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Return(False) + }, + Return(EnsureDictionary.Invoke("ContainsKey", new ParameterReferenceSnippet(keyParam))) + }); + } + + private MethodProvider BuildAdd() + { + var keyParam = new ParameterProvider("key", $"The key.", _tKey); + var valueParam = new ParameterProvider("value", $"The value to add.", _tValue); + var signature = GetSignature("Add", null, parameters: new[] { keyParam, valueParam }); + return new MethodProvider(signature, new MethodBodyStatement[] + { + EnsureDictionary.Invoke("Add", new ParameterReferenceSnippet(keyParam), new ParameterReferenceSnippet(valueParam)).ToStatement() + }); + } + + private MethodProvider BuildRemovePair() + { + var itemParam = new ParameterProvider("item", $"The item to remove.", _keyValuePair); + var item = new ParameterReferenceSnippet(itemParam); + var signature = GetSignature("Remove", typeof(bool), parameters: new[] { itemParam }); + return new MethodProvider(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Return(False) + }, + Return(EnsureDictionary.Invoke("Remove", item)) + }); + } + + private MethodProvider BuildCopyTo() + { + //TODO: This line will not honor the generic type of the array + var arrayParam = new ParameterProvider("array", $"The array to copy.", typeof(KeyValuePair<,>).MakeArrayType()); + var array = new ParameterReferenceSnippet(arrayParam); + var indexParam = new ParameterProvider("index", $"The index.", typeof(int)); + var index = new ParameterReferenceSnippet(indexParam); + var signature = GetSignature("CopyTo", null, parameters: new[] { arrayParam, indexParam }); + return new MethodProvider(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Return() + }, + EnsureDictionary.Invoke("CopyTo", array, index).ToStatement() + }); + } + + private MethodProvider BuildContains() + { + var itemParam = new ParameterProvider("item", $"The item to search for.", _keyValuePair); + var item = new ParameterReferenceSnippet(itemParam); + var signature = GetSignature("Contains", typeof(bool), parameters: new[] { itemParam }); + return new MethodProvider(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Return(False) + }, + Return(EnsureDictionary.Invoke("Contains", item)) + }); + } + + private MethodProvider BuildClear() + { + var signature = GetSignature("Clear", null); + return new MethodProvider(signature, new MethodBodyStatement[] + { + EnsureDictionary.Invoke("Clear").ToStatement() + }); + } + + private MethodProvider BuildAddPair() + { + var itemParam = new ParameterProvider("item", $"The item to add.", _keyValuePair); + var item = new ParameterReferenceSnippet(itemParam); + var signature = GetSignature("Add", null, parameters: new[] { itemParam }); + return new MethodProvider(signature, new MethodBodyStatement[] + { + EnsureDictionary.Invoke("Add", item).ToStatement() + }); + } + + private MethodProvider BuildGetEnumerator() + { + var signature = GetSignature("GetEnumerator", typeof(IEnumerator), MethodSignatureModifiers.None, explicitImpl: typeof(IEnumerable)); + return new MethodProvider(signature, new MethodBodyStatement[] + { + Return(This.Invoke("GetEnumerator")) + }); + } + + private MethodProvider BuildGetEnumeratorGeneric() + { + var signature = GetSignature("GetEnumerator", _IEnumerator); + return new MethodProvider(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + new DeclareLocalFunctionStatement(new CodeWriterDeclaration("enumerateEmpty"), Array.Empty(), _IEnumerator, new KeywordStatement("yield", new KeywordExpression("break", null))), + Return(new InvokeStaticMethodExpression(null, "enumerateEmpty", Array.Empty())) + }, + Return(EnsureDictionary.Invoke("GetEnumerator")) + }); + } + + private MethodProvider BuildEnsureDictionary() + { + return new MethodProvider(_ensureDictionarySignature, new MethodBodyStatement[] + { + Return(new BinaryOperatorExpression("??=", _innerDictionary, New.Instance(_dictionary))) + }); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ChangeTrackingListProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ChangeTrackingListProvider.cs new file mode 100644 index 00000000000..d795f80a938 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ChangeTrackingListProvider.cs @@ -0,0 +1,306 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Snippets; +using Microsoft.Generator.CSharp.Statements; +using static Microsoft.Generator.CSharp.Snippets.Snippet; + +namespace Microsoft.Generator.CSharp.Providers +{ + internal sealed class ChangeTrackingListProvider : TypeProvider + { + private static readonly Lazy _instance = new(() => new ChangeTrackingListProvider()); + + private class ChangeTrackingListTemplate { } + + private readonly MethodSignature _ensureListSignature; + private readonly MethodSignature _getEnumeratorSignature; + private readonly CSharpType _t; + private readonly FieldProvider _innerListField; + private readonly CSharpType _tArray; + private readonly ParameterProvider _tParam; + private readonly ParameterProvider _indexParam = new ParameterProvider("index", $"The index.", typeof(int)); + private VariableReferenceSnippet _innerList; + private readonly CSharpType _iListOfT; + private readonly CSharpType _iReadOnlyListOfT; + + private BoolSnippet IsUndefined { get; } = new BoolSnippet(new MemberExpression(This, "IsUndefined")); + private InvokeInstanceMethodExpression EnsureList { get; init; } + + public static ChangeTrackingListProvider Instance => _instance.Value; + + private ChangeTrackingListProvider() + { + _t = typeof(ChangeTrackingListTemplate<>).GetGenericArguments()[0]; + _iListOfT = new CSharpType(typeof(IList<>), _t); + _iReadOnlyListOfT = new CSharpType(typeof(IReadOnlyList<>), _t); + + _ensureListSignature = new MethodSignature("EnsureList", null, null, MethodSignatureModifiers.Public, _iListOfT, null, Array.Empty()); + _getEnumeratorSignature = new MethodSignature("GetEnumerator", null, null, MethodSignatureModifiers.Public, new CSharpType(typeof(IEnumerator<>), _t), null, Array.Empty()); + _innerListField = new FieldProvider(FieldModifiers.Private, _iListOfT, "_innerList"); + _innerList = new VariableReferenceSnippet(_iListOfT, _innerListField.Declaration); + _tArray = typeof(ChangeTrackingListTemplate<>).GetGenericArguments()[0].MakeArrayType(); + _tParam = new ParameterProvider("item", $"The item.", _t); + EnsureList = This.Invoke(_ensureListSignature); + } + + protected override string GetFileName() => Path.Combine("src", "Generated", "Internal", $"{Name}.cs"); + + protected override TypeSignatureModifiers GetDeclarationModifiers() + { + return TypeSignatureModifiers.Internal; + } + + public override string Name => "ChangeTrackingList"; + + protected override MethodProvider[] BuildConstructors() + { + var iListParam = new ParameterProvider("innerList", $"The inner list.", _iListOfT); + var iListSignature = new ConstructorSignature(Type, null, null, MethodSignatureModifiers.Public, new ParameterProvider[] { iListParam }); + var iListVariable = new ParameterReferenceSnippet(iListParam); + var iListBody = new MethodBodyStatement[] + { + new IfStatement(NotEqual(iListVariable, Null)) + { + new AssignValueStatement(_innerList, iListVariable) + } + }; + + var iReadOnlyListParam = new ParameterProvider("innerList", $"The inner list.", _iReadOnlyListOfT); + var iReadOnlyListSignature = new ConstructorSignature(Type, null, null, MethodSignatureModifiers.Public, new ParameterProvider[] { iReadOnlyListParam }); + var iReadOnlyListVariable = new ParameterReferenceSnippet(iReadOnlyListParam); + var iReadOnlyListBody = new MethodBodyStatement[] + { + new IfStatement(NotEqual(iReadOnlyListVariable, Null)) + { + new AssignValueStatement(_innerList, Linq.ToList(iReadOnlyListVariable)) + } + }; + + return + [ + new MethodProvider(new ConstructorSignature(Type, null, null, MethodSignatureModifiers.Public, Array.Empty()), EmptyStatement), + new MethodProvider(iListSignature, iListBody), + new MethodProvider(iReadOnlyListSignature, iReadOnlyListBody) + ]; + } + + protected override CSharpType[] BuildTypeArguments() + { + return new[] { _t }; + } + + protected override CSharpType[] BuildImplements() + { + return new[] { _iListOfT, _iReadOnlyListOfT }; + } + + protected override FieldProvider[] BuildFields() + { + return new[] { _innerListField }; + } + + protected override PropertyProvider[] BuildProperties() => + new[] + { + new PropertyProvider(null, MethodSignatureModifiers.Public, typeof(bool), "IsUndefined", new ExpressionPropertyBody(Equal(_innerList, Null))), + BuildCount(), + BuildIsReadOnly(), + BuildIndexer() + }; + + private PropertyProvider BuildIsReadOnly() + { + return new PropertyProvider($"Gets the IsReadOnly", MethodSignatureModifiers.Public, typeof(bool), "IsReadOnly", + new ExpressionPropertyBody(new TernaryConditionalExpression( + IsUndefined, + False, + new MemberExpression(EnsureList, "IsReadOnly")))); + } + + private PropertyProvider BuildCount() + { + return new PropertyProvider(null, MethodSignatureModifiers.Public, typeof(int), "Count", + new ExpressionPropertyBody(new TernaryConditionalExpression( + IsUndefined, + Literal(0), + new MemberExpression(EnsureList, "Count")))); + } + + private PropertyProvider BuildIndexer() + { + var indexParam = new ParameterProvider("index", $"The inner list.", typeof(int)); + return new IndexerProvider(null, MethodSignatureModifiers.Public, _t, indexParam, new MethodPropertyBody( + new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Throw(New.Instance(typeof(ArgumentOutOfRangeException), Nameof(new ParameterReferenceSnippet(_indexParam)))) + }, + Return(new ArrayElementExpression(EnsureList, new ParameterReferenceSnippet(_indexParam))), + }, + new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Throw(New.Instance(typeof(ArgumentOutOfRangeException), Nameof(new ParameterReferenceSnippet(_indexParam)))) + }, + new AssignValueStatement( + new ArrayElementExpression(EnsureList, new ParameterReferenceSnippet(_indexParam)), + new KeywordExpression("value", null)) + })); + } + + protected override MethodProvider[] BuildMethods() + { + return new MethodProvider[] + { + BuildReset(), + BuildGetEnumeratorOfT(), + BuildGetEnumerator(), + BuildAdd(), + BuildClear(), + BuildContains(), + BuildCopyTo(), + BuildRemove(), + BuildIndexOf(), + BuildInsert(), + BuildRemoveAt(), + BuildEnsureList() + }; + } + + private MethodProvider BuildRemoveAt() + { + var indexVariable = new ParameterReferenceSnippet(_indexParam); + return new MethodProvider(new MethodSignature("RemoveAt", null, null, MethodSignatureModifiers.Public, null, null, new ParameterProvider[] { _indexParam }), new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Throw(New.Instance(typeof(ArgumentOutOfRangeException), Nameof(indexVariable))) + }, + new InvokeInstanceMethodStatement(EnsureList, "RemoveAt", new ValueExpression[] { indexVariable }, false) + }); + } + + private MethodProvider BuildInsert() + { + return new MethodProvider(new MethodSignature("Insert", null, null, MethodSignatureModifiers.Public, null, null, new ParameterProvider[] { _indexParam, _tParam }), new MethodBodyStatement[] + { + new InvokeInstanceMethodStatement(EnsureList, "Insert", new ValueExpression[] { new ParameterReferenceSnippet(_indexParam), new ParameterReferenceSnippet(_tParam) }, false) + }); + } + + private MethodProvider BuildIndexOf() + { + var signature = new MethodSignature("IndexOf", null, null, MethodSignatureModifiers.Public, typeof(int), null, new ParameterProvider[] { _tParam }); + return new MethodProvider(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Return(Literal(-1)) + }, + Return(EnsureList.Invoke(signature)) + }); + } + + private MethodProvider BuildRemove() + { + var signature = new MethodSignature("Remove", null, null, MethodSignatureModifiers.Public, typeof(bool), null, new ParameterProvider[] { _tParam }); + return new MethodProvider(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Return(False) + }, + Return(EnsureList.Invoke(signature)) + }); + } + + private MethodProvider BuildCopyTo() + { + var arrayParam = new ParameterProvider("array", $"The array to copy to.", _tArray); + var arrayIndexParam = new ParameterProvider("arrayIndex", $"The array index.", typeof(int)); + return new MethodProvider(new MethodSignature("CopyTo", null, null, MethodSignatureModifiers.Public, null, null, new ParameterProvider[] { arrayParam, arrayIndexParam }), new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Return() + }, + new InvokeInstanceMethodStatement(EnsureList, "CopyTo", new ValueExpression[] { new ParameterReferenceSnippet(arrayParam), new ParameterReferenceSnippet(arrayIndexParam) }, false) + }); + } + + private MethodProvider BuildContains() + { + var signature = new MethodSignature("Contains", null, null, MethodSignatureModifiers.Public, typeof(bool), null, new ParameterProvider[] { _tParam }); + return new MethodProvider(signature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + Return(False) + }, + Return(EnsureList.Invoke(signature)) + }); + } + + private MethodProvider BuildClear() + { + return new MethodProvider(new MethodSignature("Clear", null, null, MethodSignatureModifiers.Public, null, null, Array.Empty()), new MethodBodyStatement[] + { + new InvokeInstanceMethodStatement(EnsureList, "Clear") + }); + } + + private MethodProvider BuildAdd() + { + var genericParameter = new ParameterProvider("item", $"The item to add.", _t); + return new MethodProvider(new MethodSignature("Add", null, null, MethodSignatureModifiers.Public, null, null, new ParameterProvider[] { genericParameter }), new MethodBodyStatement[] + { + new InvokeInstanceMethodStatement(EnsureList, "Add", new ParameterReferenceSnippet(genericParameter)) + }); + } + + private MethodProvider BuildGetEnumerator() + { + return new MethodProvider(new MethodSignature("GetEnumerator", null, null, MethodSignatureModifiers.None, typeof(IEnumerator), null, Array.Empty(), ExplicitInterface: typeof(IEnumerable)), new MethodBodyStatement[] + { + Return(This.Invoke(_getEnumeratorSignature)) + }); + } + + private MethodProvider BuildGetEnumeratorOfT() + { + return new MethodProvider(_getEnumeratorSignature, new MethodBodyStatement[] + { + new IfStatement(IsUndefined) + { + new DeclareLocalFunctionStatement(new CodeWriterDeclaration("enumerateEmpty"), Array.Empty(), new CSharpType(typeof(IEnumerator<>), _t), new KeywordStatement("yield", new KeywordExpression("break", null))), + Return(new InvokeStaticMethodExpression(null, "enumerateEmpty", Array.Empty())) + }, + Return(EnsureList.Invoke(_getEnumeratorSignature)) + }); + } + + private MethodProvider BuildReset() + { + return new MethodProvider(new MethodSignature("Reset", null, null, MethodSignatureModifiers.Public, null, null, Array.Empty()), new MethodBodyStatement[] + { + Assign(_innerList, Null) + }); + } + + private MethodProvider BuildEnsureList() + { + return new MethodProvider(_ensureListSignature, new MethodBodyStatement[] + { + Return(new BinaryOperatorExpression("??=", _innerList, New.Instance(new CSharpType(typeof(List<>), _t)))) + }); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ClientTypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ClientProvider.cs similarity index 67% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ClientTypeProvider.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ClientProvider.cs index a167f936fe0..5b93a81300f 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/ClientTypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ClientProvider.cs @@ -2,26 +2,28 @@ // Licensed under the MIT License. using System.Collections.Generic; +using System.IO; using Microsoft.Generator.CSharp.Input; -namespace Microsoft.Generator.CSharp +namespace Microsoft.Generator.CSharp.Providers { - public sealed class ClientTypeProvider : TypeProvider + public sealed class ClientProvider : TypeProvider { private readonly InputClient _inputClient; public override string Name { get; } - public ClientTypeProvider(InputClient inputClient, SourceInputModel? sourceInputModel) - : base(sourceInputModel) + public ClientProvider(InputClient inputClient) { _inputClient = inputClient; Name = inputClient.Name.ToCleanName(); } - protected override CSharpMethod[] BuildMethods() + protected override string GetFileName() => Path.Combine("src", "Generated", $"{Name}.cs"); + + protected override MethodProvider[] BuildMethods() { - List methods = new List(); + List methods = new List(); // Build methods for all the operations foreach (var operation in _inputClient.Operations) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/EnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/EnumProvider.cs new file mode 100644 index 00000000000..99aebabc459 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/EnumProvider.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Input; + +namespace Microsoft.Generator.CSharp.Providers +{ + public abstract class EnumProvider : TypeProvider + { + public static EnumProvider Create(InputEnumType input) + => input.IsExtensible + ? new ExtensibleEnumProvider(input) + : new FixedEnumProvider(input); + + protected EnumProvider(InputEnumType input) + { + _deprecated = input.Deprecated; + + IsExtensible = input.IsExtensible; + Name = input.Name.ToCleanName(); + Namespace = GetDefaultModelNamespace(CodeModelPlugin.Instance.Configuration.Namespace); + ValueType = CodeModelPlugin.Instance.TypeFactory.CreateCSharpType(input.ValueType); + IsStringValueType = ValueType.Equals(typeof(string)); + IsIntValueType = ValueType.Equals(typeof(int)) || ValueType.Equals(typeof(long)); + IsFloatValueType = ValueType.Equals(typeof(float)) || ValueType.Equals(typeof(double)); + IsNumericValueType = IsIntValueType || IsFloatValueType; + + Description = input.Description != null ? FormattableStringHelpers.FromString(input.Description) : FormattableStringHelpers.Empty; + } + + protected override string GetFileName() => Path.Combine("src", "Generated", "Models", $"{Name}.cs"); + + public CSharpType ValueType { get; } + public bool IsExtensible { get; } + internal bool IsIntValueType { get; } + internal bool IsFloatValueType { get; } + internal bool IsStringValueType { get; } + internal bool IsNumericValueType { get; } + public override string Name { get; } + public override string Namespace { get; } + public override FormattableString Description { get; } + + /// + /// The serialization provider for this enum. + /// + public TypeProvider? Serialization { get; protected init; } + + private IReadOnlyList? _members; + public IReadOnlyList Members => _members ??= BuildMembers(); + + protected abstract IReadOnlyList BuildMembers(); + + public abstract ValueExpression ToSerial(ValueExpression enumExpression); + + public abstract ValueExpression ToEnum(ValueExpression valueExpression); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ExtensibleEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ExtensibleEnumProvider.cs new file mode 100644 index 00000000000..010e42f60d9 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ExtensibleEnumProvider.cs @@ -0,0 +1,259 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Snippets; +using Microsoft.Generator.CSharp.Statements; +using static Microsoft.Generator.CSharp.Snippets.Snippet; + +namespace Microsoft.Generator.CSharp.Providers +{ + internal sealed class ExtensibleEnumProvider : EnumProvider + { + private readonly IReadOnlyList _allowedValues; + private readonly TypeSignatureModifiers _modifiers; + + internal ExtensibleEnumProvider(InputEnumType input): base(input) + { + _allowedValues = input.Values; + + // extensible enums are implemented as readonly structs + _modifiers = TypeSignatureModifiers.Partial | TypeSignatureModifiers.ReadOnly | TypeSignatureModifiers.Struct; + if (input.Accessibility == "internal") + { + _modifiers |= TypeSignatureModifiers.Internal; + } + + _valueField = new FieldProvider(FieldModifiers.Private | FieldModifiers.ReadOnly, ValueType, "_value"); + } + + private readonly FieldProvider _valueField; + + protected override TypeSignatureModifiers GetDeclarationModifiers() => _modifiers; + + protected override IReadOnlyList BuildMembers() + { + var values = new EnumTypeMember[_allowedValues.Count]; + + for (int i = 0; i < _allowedValues.Count; i++) + { + var inputValue = _allowedValues[i]; + // build the field + var modifiers = FieldModifiers.Private | FieldModifiers.Const; + // the fields for extensible enums are private and const, storing the underlying values, therefore we need to append the word `Value` to the name + var valueName = inputValue.Name.ToCleanName(); + var name = $"{valueName}Value"; + // for initializationValue, if the enum is extensible, we always need it + var initializationValue = Literal(inputValue.Value); + var field = new FieldProvider( + modifiers, + ValueType, + name, + FormattableStringHelpers.FromString(inputValue.Description), + initializationValue); + + values[i] = new EnumTypeMember(valueName, field, inputValue.Value); + } + + return values; + } + + protected override CSharpType[] BuildImplements() + => [new CSharpType(typeof(IEquatable<>), Type)]; // extensible enums implement IEquatable + + protected override FieldProvider[] BuildFields() + => [_valueField, .. Members.Select(v => v.Field)]; + + protected override PropertyProvider[] BuildProperties() + { + var properties = new PropertyProvider[Members.Count]; + + var index = 0; + foreach (var enumValue in Members) + { + var name = enumValue.Name; + var value = enumValue.Value; + var field = enumValue.Field; + properties[index++] = new PropertyProvider( + description: field.Description, + modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, + type: Type, + name: name, + body: new AutoPropertyBody(false, InitializationExpression: New.Instance(Type, field))); + } + + return properties; + } + + protected override MethodProvider[] BuildConstructors() + { + var validation = ValueType.IsValueType ? ParameterValidationType.None : ParameterValidationType.AssertNotNull; + var valueParameter = new ParameterProvider("value", $"The value.", ValueType) + { + Validation = validation + }; + var signature = new ConstructorSignature( + Type: Type, + Summary: null, + Description: $"Initializes a new instance of {Type:C}.", + Modifiers: MethodSignatureModifiers.Public, + Parameters: [valueParameter]); + + var valueField = (ValueExpression)_valueField; + var body = new MethodBodyStatement[] + { + new ParameterValidationStatement(signature.Parameters), + Assign(valueField, valueParameter) + }; + + return [new MethodProvider(signature, body, CSharpMethodKinds.Constructor)]; + } + + protected override MethodProvider[] BuildMethods() + { + var methods = new List(); + + var leftParameter = new ParameterProvider("left", $"The left value to compare.", Type); + var rightParameter = new ParameterProvider("right", $"The right value to compare.", Type); + var left = (ValueExpression)leftParameter; + var right = (ValueExpression)rightParameter; + var equalitySignature = new MethodSignature( + Name: "==", + Summary: null, + Description: $"Determines if two {Type:C} values are the same.", + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Static | MethodSignatureModifiers.Operator, + ReturnType: typeof(bool), + ReturnDescription: null, + Parameters: [leftParameter, rightParameter]); + + methods.Add(new(equalitySignature, left.InvokeEquals(right))); + + var inequalitySignature = equalitySignature with + { + Name = "!=", + Description = $"Determines if two {Type:C} values are not the same.", + }; + + methods.Add(new(inequalitySignature, Not(left.InvokeEquals(right)))); + + var valueParameter = new ParameterProvider("value", $"The value.", ValueType); + var castSignature = new MethodSignature( + Name: string.Empty, + Summary: null, + Description: $"Converts a string to a {Type:C}", + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Static | MethodSignatureModifiers.Implicit | MethodSignatureModifiers.Operator, + ReturnType: Type, + ReturnDescription: null, + Parameters: [valueParameter]); + + methods.Add(new(castSignature, New.Instance(Type, valueParameter))); + + var objParameter = new ParameterProvider("obj", $"The object to compare.", typeof(object)); + var equalsSignature = new MethodSignature( + Name: nameof(object.Equals), + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Override, + ReturnType: typeof(bool), + ReturnDescription: null, + Parameters: [objParameter], + Attributes: [new AttributeStatement(typeof(EditorBrowsableAttribute), FrameworkEnumValue(EditorBrowsableState.Never))]); + + // writes the method: + // public override bool Equals(object obj) => obj is EnumType other && Equals(other); + methods.Add(new(equalsSignature, And(Is(objParameter, new DeclarationExpression(Type, "other", out var other)), new BoolSnippet(new InvokeInstanceMethodExpression(null, nameof(object.Equals), [other]))))); + + var otherParameter = new ParameterProvider("other", $"The instance to compare.", Type); + equalsSignature = equalsSignature with + { + Modifiers = MethodSignatureModifiers.Public, + Parameters = [otherParameter], + Attributes = Array.Empty() + }; + + // writes the method: + // public bool Equals(EnumType other) => string.Equals(_value, other._value, StringComparison.InvariantCultureIgnoreCase); + // or + // public bool Equals(EnumType other) => int/float.Equals(_value, other._value); + var valueField = new VariableReferenceSnippet(ValueType.WithNullable(!ValueType.IsValueType), _valueField.Declaration); + var otherValue = ((ValueExpression)otherParameter).Property(_valueField.Name); + var equalsExpressionBody = IsStringValueType + ? new InvokeStaticMethodExpression(ValueType, nameof(object.Equals), [valueField, otherValue, FrameworkEnumValue(StringComparison.InvariantCultureIgnoreCase)]) + : new InvokeStaticMethodExpression(ValueType, nameof(object.Equals), [valueField, otherValue]); + methods.Add(new(equalsSignature, equalsExpressionBody)); + + var getHashCodeSignature = new MethodSignature( + Name: nameof(object.GetHashCode), + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Override, + ReturnType: typeof(int), + ReturnDescription: null, + Parameters: Array.Empty()); + + // writes the method: + // for string + // public override int GetHashCode() => _value?.GetHashCode() ?? 0; + // for others + // public override int GetHashCode() => _value.GetHashCode(); + var getHashCodeExpressionBody = IsStringValueType + ? NullCoalescing(valueField.NullConditional().InvokeGetHashCode(), Int(0)) + : valueField.Untyped.InvokeGetHashCode(); + methods.Add(new(getHashCodeSignature, getHashCodeExpressionBody)); + + var toStringSignature = new MethodSignature( + Name: nameof(object.ToString), + Summary: null, + Description: null, + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Override, + ReturnType: typeof(string), + ReturnDescription: null, + Parameters: Array.Empty()); + + // writes the method: + // for string + // public override string ToString() => _value; + // for others + // public override string ToString() => _value.ToString(CultureInfo.InvariantCulture); + ValueExpression toStringExpressionBody = IsStringValueType + ? valueField + : valueField.Untyped.Invoke(nameof(object.ToString), new MemberExpression(typeof(CultureInfo), nameof(CultureInfo.InvariantCulture))); + methods.Add(new(toStringSignature, toStringExpressionBody)); + + // for string-based extensible enums, we are using `ToString` as its serialization + // for non-string-based extensible enums, we need a method to serialize them + if (!IsStringValueType) + { + var toSerialSignature = new MethodSignature( + Name: $"ToSerial{ValueType.Name}", + Modifiers: MethodSignatureModifiers.Internal, + ReturnType: ValueType, + Parameters: Array.Empty(), + Summary: null, Description: null, ReturnDescription: null); + + // writes the method: + // internal float ToSerialSingle() => _value; // when ValueType is float + // internal int ToSerialInt32() => _value; // when ValueType is int + // etc + methods.Add(new(toSerialSignature, valueField)); + } + + return methods.ToArray(); + } + + public override ValueExpression ToSerial(ValueExpression enumExpression) + { + var serialMethodName = IsStringValueType ? nameof(object.ToString) : $"ToSerial{ValueType.Name}"; + return enumExpression.Invoke(serialMethodName); + } + + public override ValueExpression ToEnum(ValueExpression valueExpression) + => New.Instance(Type, valueExpression); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/FieldProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/FieldProvider.cs new file mode 100644 index 00000000000..e7ac24e2a95 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/FieldProvider.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Providers +{ + public sealed class FieldProvider + { + public FormattableString? Description { get; } + public FieldModifiers Modifiers { get; } + public CSharpType Type { get; } + public string Name { get; } + public ValueExpression? InitializationValue { get; } + + private CodeWriterDeclaration? _declaration; + + public CodeWriterDeclaration Declaration => _declaration ??= new CodeWriterDeclaration(Name); + + public FieldProvider( + FieldModifiers modifiers, + CSharpType type, + string name, + FormattableString? description = null, + ValueExpression? initializationValue = null) + { + Modifiers = modifiers; + Type = type; + Name = name; + Description = description; + InitializationValue = initializationValue; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/FixedEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/FixedEnumProvider.cs new file mode 100644 index 00000000000..20f0737cb1b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/FixedEnumProvider.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Input; +using static Microsoft.Generator.CSharp.Snippets.Snippet; + +namespace Microsoft.Generator.CSharp.Providers +{ + internal sealed class FixedEnumProvider : EnumProvider + { + private readonly IReadOnlyList _allowedValues; + private readonly TypeSignatureModifiers _modifiers; + + internal FixedEnumProvider(InputEnumType input) : base(input) + { + _allowedValues = input.Values; + + // fixed enums are implemented by enum in C# + _modifiers = TypeSignatureModifiers.Enum; + if (input.Accessibility == "internal") + { + _modifiers |= TypeSignatureModifiers.Internal; + } + + Serialization = new FixedEnumSerializationProvider(this); + } + + protected override TypeSignatureModifiers GetDeclarationModifiers() => _modifiers; + + // we have to build the values first, because the corresponding fieldDeclaration of the values might need all of the existing values to avoid name conflicts + protected override IReadOnlyList BuildMembers() + { + var values = new EnumTypeMember[_allowedValues.Count]; + for (int i = 0; i < _allowedValues.Count; i++) + { + var inputValue = _allowedValues[i]; + var modifiers = FieldModifiers.Public | FieldModifiers.Static; + // the fields for fixed enums are just its members (we use fields to represent the values in a system `enum` type), we just use the name for this field + var name = inputValue.Name.ToCleanName(); + // for fixed enum, we only need it for int values, for other value typed fixed enum, we use the serialization extension method to give the values (because assigning them to enum members cannot compile) + var initializationValue = IsIntValueType ? Literal(inputValue.Value) : null; + var field = new FieldProvider( + modifiers, + ValueType, + name, + FormattableStringHelpers.FromString(inputValue.Description), + initializationValue); + + values[i] = new EnumTypeMember(name, field, inputValue.Value); + } + return values; + } + + protected override FieldProvider[] BuildFields() + => Members.Select(v => v.Field).ToArray(); + + public override ValueExpression ToSerial(ValueExpression enumExpression) + { + if (IsIntValueType) + { + // when the fixed enum is implemented as int, we cast to the value + return enumExpression.CastTo(ValueType); + } + + // otherwise we call the corresponding extension method to convert the value + return new InvokeStaticMethodExpression(Serialization?.Type, $"ToSerial{ValueType.Name}", [enumExpression], CallAsExtension: true); + } + + public override ValueExpression ToEnum(ValueExpression valueExpression) + => new InvokeStaticMethodExpression(Serialization?.Type, $"To{Type.Name}", [valueExpression], CallAsExtension: true); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/FixedEnumSerializationProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/FixedEnumSerializationProvider.cs new file mode 100644 index 00000000000..91b88a2ce5f --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/FixedEnumSerializationProvider.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Snippets; +using Microsoft.Generator.CSharp.Statements; +using static Microsoft.Generator.CSharp.Snippets.Snippet; + +namespace Microsoft.Generator.CSharp.Providers +{ + /// + /// This defines a class with extension methods for enums to convert an enum to its underlying value, or from its underlying value to an instance of the enum + /// + internal class FixedEnumSerializationProvider : TypeProvider + { + private readonly EnumProvider _enumType; + + public FixedEnumSerializationProvider(EnumProvider enumType) + { + Debug.Assert(!enumType.IsExtensible); + + _enumType = enumType; + Namespace = _enumType.Namespace; + Name = $"{_enumType.Name}Extensions"; + } + + protected override string GetFileName() => Path.Combine("src", "Generated", "Models", $"{Name}.cs"); + + protected override TypeSignatureModifiers GetDeclarationModifiers() => TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static | TypeSignatureModifiers.Partial; + + public override string Namespace { get; } + + public override string Name { get; } + + /// + /// Returns if this enum type needs an extension method for serialization + /// + /// + private bool NeedsSerializationMethod() + { + // fixed enum with int based types, we do not write a method for serialization because it was embedded in the definition + if (_enumType is { IsExtensible: false, IsIntValueType: true }) + return false; + + // otherwise we need a serialization method with the name of `ToSerial{UnderlyingTypeName}` + return true; + } + + protected override MethodProvider[] BuildMethods() + { + var methods = new List(); + // serialization method (in some cases we do not need serialization) + if (NeedsSerializationMethod()) + { + var serializationValueParameter = new ParameterProvider("value", $"The value to serialize.", _enumType.Type); + var serializationSignature = new MethodSignature( + Name: $"ToSerial{_enumType.ValueType.Name}", + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Static | MethodSignatureModifiers.Extension, + ReturnType: _enumType.ValueType, + Parameters: [serializationValueParameter], + Summary: null, Description: null, ReturnDescription: null); + + // the fields of an enum type are the values of the enum type + var knownCases = new SwitchCaseExpression[_enumType.Members.Count]; + for (int i = 0; i < knownCases.Length; i++) + { + var enumValue = _enumType.Members[i]; + knownCases[i] = new SwitchCaseExpression(new MemberExpression(_enumType.Type, enumValue.Field.Name), Literal(enumValue.Value)); + } + var defaultCase = SwitchCaseExpression.Default(ThrowExpression(New.ArgumentOutOfRangeException(_enumType, serializationValueParameter))); + var serializationBody = new SwitchExpression(serializationValueParameter, [.. knownCases, defaultCase]); + methods.Add(new(serializationSignature, serializationBody)); + } + + // deserialization method (we always need a deserialization) + var deserializationValueParameter = new ParameterProvider("value", $"The value to deserialize.", _enumType.ValueType); + var deserializationSignature = new MethodSignature( + Name: $"To{_enumType.Type.Name}", + Modifiers: MethodSignatureModifiers.Public | MethodSignatureModifiers.Static | MethodSignatureModifiers.Extension, + ReturnType: _enumType.Type, + Parameters: [deserializationValueParameter], + Summary: null, Description: null, ReturnDescription: null); + + var value = (ValueExpression)deserializationValueParameter; + var stringComparer = new MemberExpression(typeof(StringComparer), nameof(StringComparer.OrdinalIgnoreCase)); + var deserializationBody = new List(); + + // in general, this loop builds up if statements for each value, it looks like: + // if () { return EnumType.TheValue; } + // the condition could be different depending on the type of the underlying value type of the enum + for (int i = 0; i < _enumType.Fields.Count; i++) + { + var enumField = _enumType.Fields[i]; + var enumValue = _enumType.Members[i]; + BoolSnippet condition; + if (_enumType.IsStringValueType) + { + // when the values are strings, we compare them case-insensitively + // this is either + // StringComparer.OrdinalIgnoreCase.Equals(value, "") + // or + // string.Equals(value, "", StringComparison.InvariantCultureIgnoreCase) + condition = new(enumValue.Value is string strValue && strValue.All(char.IsAscii) + ? stringComparer.Invoke(nameof(IEqualityComparer.Equals), value, Literal(strValue)) + : new InvokeStaticMethodExpression(_enumType.ValueType, nameof(object.Equals), [value, Literal(enumValue.Value), FrameworkEnumValue(StringComparison.InvariantCultureIgnoreCase)])); + } + else + { + // when the values are not strings (it should be numbers), we just compare them using `==` operator, like `value == ` + condition = Equal(value, Literal(enumValue.Value)); + } + deserializationBody.Add(new IfStatement(condition) + { + Return(new MemberExpression(_enumType.Type, enumField.Name)) + }); + } + + // add a fallback throw statement to ensure every path of this method returns a value + deserializationBody.Add(Throw(New.ArgumentOutOfRangeException(_enumType, deserializationValueParameter))); + + methods.Add(new(deserializationSignature, deserializationBody)); + + return methods.ToArray(); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/IndexerProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/IndexerProvider.cs new file mode 100644 index 00000000000..9410fdc66b3 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/IndexerProvider.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Generator.CSharp.Providers +{ + internal class IndexerProvider : PropertyProvider + { + public ParameterProvider IndexerParameter { get; } + public IndexerProvider(FormattableString? description, MethodSignatureModifiers modifiers, CSharpType propertyType, ParameterProvider indexerParameter, PropertyBody propertyBody, IReadOnlyDictionary? exceptions = null, CSharpType? explicitInterface = null) + : base(description, modifiers, propertyType, "this", propertyBody, exceptions, explicitInterface) + { + IndexerParameter = indexerParameter; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/CSharpMethod.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/MethodProvider.cs similarity index 69% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/CSharpMethod.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/MethodProvider.cs index df7cceb0847..23bdcc6b032 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/CSharpMethod.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/MethodProvider.cs @@ -2,13 +2,14 @@ // Licensed under the MIT License. using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Statements; -namespace Microsoft.Generator.CSharp +namespace Microsoft.Generator.CSharp.Providers { /// /// Represents a C# method consisting of a signature, body, and expression. /// - public sealed class CSharpMethod + public sealed class MethodProvider { /// /// The kind of method of type . @@ -19,12 +20,12 @@ public sealed class CSharpMethod public ValueExpression? BodyExpression { get; } /// - /// Initializes a new instance of the class with a body statement and method signature. + /// Initializes a new instance of the class with a body statement and method signature. /// /// The method signature. /// The method body. /// The method kind . - public CSharpMethod(MethodSignatureBase signature, MethodBodyStatement bodyStatements, CSharpMethodKinds? kind = null) + public MethodProvider(MethodSignatureBase signature, MethodBodyStatement bodyStatements, CSharpMethodKinds? kind = null) { Signature = signature; BodyStatements = bodyStatements; @@ -32,12 +33,12 @@ public CSharpMethod(MethodSignatureBase signature, MethodBodyStatement bodyState } /// - /// Initializes a new instance of the class with a body expression and method signature. + /// Initializes a new instance of the class with a body expression and method signature. /// /// The method signature. /// The method body expression. /// The method kind . - public CSharpMethod(MethodSignatureBase signature, ValueExpression bodyExpression, CSharpMethodKinds? kind = null) + public MethodProvider(MethodSignatureBase signature, ValueExpression bodyExpression, CSharpMethodKinds? kind = null) { Signature = signature; BodyExpression = bodyExpression; diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs new file mode 100644 index 00000000000..38ef66fe0a5 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ModelProvider.cs @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Snippets; +using Microsoft.Generator.CSharp.Statements; +using static Microsoft.Generator.CSharp.Snippets.Snippet; + +namespace Microsoft.Generator.CSharp.Providers +{ + public sealed class ModelProvider : TypeProvider + { + private readonly InputModelType _inputModel; + public override string Name { get; } + public override string Namespace { get; } + public override FormattableString Description { get; } + + private readonly bool _isStruct; + private readonly TypeSignatureModifiers _declarationModifiers; + + /// + /// The serializations providers for the model provider. + /// + public IReadOnlyList SerializationProviders { get; } = Array.Empty(); + + protected override string GetFileName() => Path.Combine("src", "Generated", "Models", $"{Name}.cs"); + + public ModelProvider(InputModelType inputModel) + { + _inputModel = inputModel; + Name = inputModel.Name.ToCleanName(); + Namespace = GetDefaultModelNamespace(CodeModelPlugin.Instance.Configuration.Namespace); + Description = inputModel.Description != null ? FormattableStringHelpers.FromString(inputModel.Description) : FormattableStringHelpers.Empty; + // TODO -- support generating models as structs. Tracking issue: https://github.com/microsoft/typespec/issues/3453 + _declarationModifiers = TypeSignatureModifiers.Partial | TypeSignatureModifiers.Class; + if (inputModel.Accessibility == "internal") + { + _declarationModifiers |= TypeSignatureModifiers.Internal; + } + + bool isAbstract = inputModel.DiscriminatorPropertyName is not null && inputModel.DiscriminatorValue is null; + if (isAbstract) + { + _declarationModifiers |= TypeSignatureModifiers.Abstract; + } + + if (inputModel.Usage.HasFlag(InputModelTypeUsage.Json)) + { + SerializationProviders = CodeModelPlugin.Instance.GetSerializationTypeProviders(this, _inputModel); + } + + _isStruct = false; // this is only a temporary placeholder because we do not support to generate structs yet. + } + + protected override TypeSignatureModifiers GetDeclarationModifiers() => _declarationModifiers; + + protected override PropertyProvider[] BuildProperties() + { + var propertiesCount = _inputModel.Properties.Count; + var propertyDeclarations = new PropertyProvider[propertiesCount]; + + for (int i = 0; i < propertiesCount; i++) + { + var property = _inputModel.Properties[i]; + propertyDeclarations[i] = new PropertyProvider(property); + } + + return propertyDeclarations; + } + + protected override MethodProvider[] BuildConstructors() + { + if (_inputModel.IsUnknownDiscriminatorModel) + { + return Array.Empty(); + } + + // Build the initialization constructor + var accessibility = DeclarationModifiers.HasFlag(TypeSignatureModifiers.Abstract) + ? MethodSignatureModifiers.Protected + : _inputModel.Usage.HasFlag(InputModelTypeUsage.Input) + ? MethodSignatureModifiers.Public + : MethodSignatureModifiers.Internal; + var constructorParameters = BuildConstructorParameters(); + + var constructor = new MethodProvider( + signature: new ConstructorSignature( + Type, + $"Initializes a new instance of {Type:C}", + null, + accessibility, + constructorParameters), + bodyStatements: new MethodBodyStatement[] + { + new ParameterValidationStatement(constructorParameters), + GetPropertyInitializers(constructorParameters) + }, + kind: CSharpMethodKinds.Constructor); + + return new MethodProvider[] { constructor }; + } + + private IReadOnlyList BuildConstructorParameters() + { + List constructorParameters = new List(); + + foreach (var property in _inputModel.Properties) + { + CSharpType propertyType = CodeModelPlugin.Instance.TypeFactory.CreateCSharpType(property.Type); + if (_isStruct || (property is { IsRequired: true, IsDiscriminator: false } && !propertyType.IsLiteral)) + { + if (!property.IsReadOnly) + { + constructorParameters.Add(new ParameterProvider(property) + { + Type = propertyType.InputType + }); + } + } + } + + return constructorParameters; + } + + private MethodBodyStatement GetPropertyInitializers(IReadOnlyList parameters) + { + List methodBodyStatements = new(); + + Dictionary parameterMap = parameters.ToDictionary( + parameter => parameter.Name, + parameter => parameter); + + foreach (var property in Properties) + { + ValueExpression? initializationValue = null; + + if (parameterMap.TryGetValue(property.Name.ToVariableName(), out var parameter) || _isStruct) + { + if (parameter != null) + { + initializationValue = new ParameterReferenceSnippet(parameter); + + if (CSharpType.RequiresToList(parameter.Type, property.Type)) + { + initializationValue = parameter.Type.IsNullable ? + Linq.ToList(new NullConditionalExpression(initializationValue)) : + Linq.ToList(initializationValue); + } + } + } + else if (initializationValue == null && property.Type.IsCollection) + { + // TO-DO: Properly initialize collection properties - https://github.com/microsoft/typespec/issues/3509 + initializationValue = New.Instance(property.Type.PropertyInitializationType); + } + + if (initializationValue != null) + { + methodBodyStatements.Add(Assign(new MemberExpression(null, property.Name), initializationValue)); + } + } + + return methodBodyStatements; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/OptionalProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/OptionalProvider.cs new file mode 100644 index 00000000000..2ef8468cee2 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/OptionalProvider.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Snippets; +using Microsoft.Generator.CSharp.Statements; +using static Microsoft.Generator.CSharp.Snippets.Snippet; + +namespace Microsoft.Generator.CSharp.Providers +{ + internal class OptionalProvider : TypeProvider + { + private static readonly Lazy _instance = new(() => new OptionalProvider()); + public static OptionalProvider Instance => _instance.Value; + + private class ListTemplate { } + + private readonly CSharpType _t = typeof(ListTemplate<>).GetGenericArguments()[0]; + private readonly CSharpType _tKey = ChangeTrackingDictionaryProvider.Instance.Type.Arguments[0]; + private readonly CSharpType _tValue = ChangeTrackingDictionaryProvider.Instance.Type.Arguments[1]; + private readonly CSharpType _genericChangeTrackingList; + private readonly CSharpType _genericChangeTrackingDictionary; + + private OptionalProvider() + { + _genericChangeTrackingList = ChangeTrackingListProvider.Instance.Type; + _genericChangeTrackingDictionary = ChangeTrackingDictionaryProvider.Instance.Type; + } + + protected override TypeSignatureModifiers GetDeclarationModifiers() + { + return TypeSignatureModifiers.Internal | TypeSignatureModifiers.Static; + } + + protected override string GetFileName() => Path.Combine("src", "Generated", "Internal", $"{Name}.cs"); + + public override string Name => "Optional"; + + protected override MethodProvider[] BuildMethods() + { + return + [ + BuildIsListDefined(), + BuildIsDictionaryDefined(), + BuildIsReadOnlyDictionaryDefined(), + IsStructDefined(), + IsObjectDefined(), + IsJsonElementDefined(), + IsStringDefined(), + ]; + } + + private MethodSignature GetIsDefinedSignature(ParameterProvider valueParam, IReadOnlyList? genericArguments = null, IReadOnlyList? genericParameterConstraints = null) => new( + "IsDefined", + null, + null, + MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, + typeof(bool), + null, + [valueParam], + GenericArguments: genericArguments, + GenericParameterConstraints: genericParameterConstraints); + + private MethodSignature GetIsCollectionDefinedSignature(ParameterProvider collectionParam, params CSharpType[] cSharpTypes) => new( + "IsCollectionDefined", + null, + null, + MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, + typeof(bool), + null, + [collectionParam], + GenericArguments: cSharpTypes); + + private MethodProvider IsStringDefined() + { + var valueParam = new ParameterProvider("value", $"The value.", typeof(string)); + var signature = GetIsDefinedSignature(valueParam); + return new MethodProvider(signature, new MethodBodyStatement[] + { + Return(NotEqual(new ParameterReferenceSnippet(valueParam), Null)) + }); + } + + private MethodProvider IsJsonElementDefined() + { + var valueParam = new ParameterProvider("value", $"The value.", typeof(JsonElement)); + var signature = GetIsDefinedSignature(valueParam); + return new MethodProvider(signature, new MethodBodyStatement[] + { + Return(new JsonElementSnippet(new ParameterReferenceSnippet(valueParam)).ValueKindNotEqualsUndefined()) + }); + } + + private MethodProvider IsObjectDefined() + { + var valueParam = new ParameterProvider("value", $"The value.", typeof(object)); + var signature = GetIsDefinedSignature(valueParam); + return new MethodProvider(signature, new MethodBodyStatement[] + { + Return(NotEqual(new ParameterReferenceSnippet(valueParam), Null)) + }); + } + + private MethodProvider IsStructDefined() + { + var valueParam = new ParameterProvider("value", $"The value.", _t.WithNullable(true)); + var signature = GetIsDefinedSignature(valueParam, new[] { _t }, new[] { Where.Struct(_t) }); + return new MethodProvider(signature, new MethodBodyStatement[] + { + Return(new MemberExpression(new ParameterReferenceSnippet(valueParam), "HasValue")) + }); + } + + private MethodProvider BuildIsReadOnlyDictionaryDefined() + { + var collectionParam = new ParameterProvider("collection", $"The value.", new CSharpType(typeof(IReadOnlyDictionary<,>), _tKey, _tValue)); + var signature = GetIsCollectionDefinedSignature(collectionParam, _tKey, _tValue); + VariableReferenceSnippet changeTrackingReference = new VariableReferenceSnippet(_genericChangeTrackingDictionary, new CodeWriterDeclaration("changeTrackingDictionary")); + DeclarationExpression changeTrackingDeclarationExpression = new(changeTrackingReference.Type, changeTrackingReference.Declaration, false); + + return new MethodProvider(signature, new MethodBodyStatement[] + { + Return(Not(BoolSnippet.Is(new ParameterReferenceSnippet(collectionParam), changeTrackingDeclarationExpression) + .And(new MemberExpression(changeTrackingReference, "IsUndefined")))) + }); + } + + private MethodProvider BuildIsDictionaryDefined() + { + var collectionParam = new ParameterProvider("collection", $"The collection.", new CSharpType(typeof(IDictionary<,>), _tKey, _tValue)); + var signature = GetIsCollectionDefinedSignature(collectionParam, _tKey, _tValue); + VariableReferenceSnippet changeTrackingReference = new VariableReferenceSnippet(_genericChangeTrackingDictionary, new CodeWriterDeclaration("changeTrackingDictionary")); + DeclarationExpression changeTrackingDeclarationExpression = new(changeTrackingReference.Type, changeTrackingReference.Declaration, false); + + return new MethodProvider(signature, new MethodBodyStatement[] + { + Return(Not(BoolSnippet.Is(new ParameterReferenceSnippet(collectionParam), changeTrackingDeclarationExpression) + .And(new MemberExpression(changeTrackingReference, "IsUndefined")))) + }); + } + + private MethodProvider BuildIsListDefined() + { + var collectionParam = new ParameterProvider("collection", $"The collection.", new CSharpType(typeof(IEnumerable<>), _t)); + var signature = GetIsCollectionDefinedSignature(collectionParam, _t); + VariableReferenceSnippet changeTrackingReference = new VariableReferenceSnippet(_genericChangeTrackingList, new CodeWriterDeclaration("changeTrackingList")); + DeclarationExpression changeTrackingDeclarationExpression = new(changeTrackingReference.Type, changeTrackingReference.Declaration, false); + + return new MethodProvider(signature, new MethodBodyStatement[] + { + Return(Not(BoolSnippet.Is(new ParameterReferenceSnippet(collectionParam), changeTrackingDeclarationExpression) + .And(new MemberExpression(changeTrackingReference, "IsUndefined")))) + }); + } + + internal BoolSnippet IsDefined(TypedSnippet value) + { + return new BoolSnippet(new InvokeStaticMethodExpression(Type, "IsDefined", [ value ])); + } + + internal BoolSnippet IsCollectionDefined(TypedSnippet collection) + { + return new BoolSnippet(new InvokeStaticMethodExpression(Type, "IsCollectionDefined", [ collection ])); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ParameterProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ParameterProvider.cs new file mode 100644 index 00000000000..bea5012de47 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/ParameterProvider.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Statements; + +namespace Microsoft.Generator.CSharp.Providers +{ + [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] + public sealed class ParameterProvider : IEquatable + { + public string Name { get; } + public FormattableString Description { get; } + public CSharpType Type { get; init; } + public ValueExpression? DefaultValue { get; init; } + public ParameterValidationType? Validation { get; init; } = ParameterValidationType.None; + public bool IsRef { get; } + public bool IsOut { get; } + internal AttributeStatement[] Attributes { get; init; } = Array.Empty(); + + public ParameterProvider(InputModelProperty inputProperty) + { + Name = inputProperty.Name.ToVariableName(); + Description = FormattableStringHelpers.FromString(inputProperty.Description); + Type = CodeModelPlugin.Instance.TypeFactory.CreateCSharpType(inputProperty.Type); + Validation = GetParameterValidation(inputProperty, Type); + } + + /// + /// Creates a from an . + /// + /// The to convert. + public ParameterProvider(InputParameter inputParameter) + { + // TO-DO: Add additional implementation to properly build the parameter https://github.com/Azure/autorest.csharp/issues/4607 + Name = inputParameter.Name; + Description = FormattableStringHelpers.FromString(inputParameter.Description) ?? FormattableStringHelpers.Empty; + Type = CodeModelPlugin.Instance.TypeFactory.CreateCSharpType(inputParameter.Type); + Validation = inputParameter.IsRequired ? ParameterValidationType.AssertNotNull : ParameterValidationType.None; + } + + public ParameterProvider( + string name, + FormattableString description, + CSharpType type, + ValueExpression? defaultValue = null, + bool isRef = false, + bool isOut = false) + { + Name = name; + Type = type; + Description = description; + IsRef = isRef; + IsOut = isOut; + DefaultValue = defaultValue; + } + + private ParameterValidationType GetParameterValidation(InputModelProperty property, CSharpType propertyType) + { + // We do not validate a parameter when it is a value type (struct or int, etc) + if (propertyType.IsValueType) + { + return ParameterValidationType.None; + } + + // or it is readonly + if (property.IsReadOnly) + { + return ParameterValidationType.None; + } + + // or it is optional + if (!property.IsRequired) + { + return ParameterValidationType.None; + } + + // or it is nullable + if (propertyType.IsNullable) + { + return ParameterValidationType.None; + } + + return ParameterValidationType.AssertNotNull; + } + + public override bool Equals(object? obj) + { + return obj is ParameterProvider parameter && Equals(parameter); + } + + public bool Equals(ParameterProvider? y) + { + if (ReferenceEquals(this, y)) + { + return true; + } + + if (this is null || y is null) + { + return false; + } + + return Type.AreNamesEqual(y.Type) && Name == y.Name && Attributes.SequenceEqual(y.Attributes); + } + + public override int GetHashCode() + { + return GetHashCode(this); + } + + private int GetHashCode([DisallowNull] ParameterProvider obj) + { + // remove type as part of the hash code generation as the type might have changes between versions + return HashCode.Combine(obj.Name); + } + + private string GetDebuggerDisplay() + { + return $"Name: {Name}, Type: {Type}"; + } + + // TO-DO: Migrate code from autorest as part of output classes migration : https://github.com/Azure/autorest.csharp/issues/4198 + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/PropertyProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/PropertyProvider.cs new file mode 100644 index 00000000000..7f0e7d61e99 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/PropertyProvider.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Snippets; + +namespace Microsoft.Generator.CSharp.Providers +{ + [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] + public class PropertyProvider + { + public IReadOnlyList Description { get; } + public MethodSignatureModifiers Modifiers { get; } + public CSharpType Type { get; } + public string Name { get; } + public PropertyBody Body { get; } + public IReadOnlyDictionary? Exceptions { get; } + public CSharpType? ExplicitInterface { get; } + public PropertyProvider(InputModelProperty inputProperty) + { + var propertyType = CodeModelPlugin.Instance.TypeFactory.CreateCSharpType(inputProperty.Type); + var serializationFormat = CodeModelPlugin.Instance.TypeFactory.GetSerializationFormat(inputProperty.Type); + var propHasSetter = PropertyHasSetter(propertyType, inputProperty); + MethodSignatureModifiers setterModifier = propHasSetter ? MethodSignatureModifiers.Public : MethodSignatureModifiers.None; + + Type = propertyType; + Modifiers = MethodSignatureModifiers.Public; + Description = PropertyDescriptionBuilder.BuildPropertyDescription(inputProperty, propertyType, serializationFormat, !propHasSetter); + Name = inputProperty.Name.FirstCharToUpperCase(); + Body = new AutoPropertyBody(propHasSetter, setterModifier, GetPropertyInitializationValue(propertyType, inputProperty)); + } + + public PropertyProvider(FormattableString? description, MethodSignatureModifiers modifiers, CSharpType type, string name, PropertyBody body, IReadOnlyDictionary? exceptions = null, CSharpType? explicitInterface = null) + { + Description = [description ?? PropertyDescriptionBuilder.CreateDefaultPropertyDescription(name, !body.HasSetter)]; + Modifiers = modifiers; + Type = type; + Name = name; + Body = body; + Exceptions = exceptions; + ExplicitInterface = explicitInterface; + } + + /// + /// Returns true if the property has a setter. + /// + /// The of the property. + private bool PropertyHasSetter(CSharpType type, InputModelProperty inputProperty) + { + if (inputProperty.IsDiscriminator) + { + return true; + } + + if (inputProperty.IsReadOnly) + { + return false; + } + + if (type.IsLiteral && inputProperty.IsRequired) + { + return false; + } + + if (type.IsCollection && !type.IsReadOnlyMemory) + { + return type.IsNullable; + } + + return true; + } + + private ValueExpression? GetPropertyInitializationValue(CSharpType propertyType, InputModelProperty inputProperty) + { + if (!inputProperty.IsRequired) + return null; + + if (propertyType.IsLiteral) + { + if (!propertyType.IsNullable) + { + return Snippet.Literal(propertyType.Literal); + } + else + { + return Snippet.DefaultOf(propertyType); + } + } + + return null; + } + + private string GetDebuggerDisplay() + { + return $"Name: {Name}, Type: {Type}"; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs similarity index 72% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/TypeProvider.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs index 8db8d16e305..8a7728d1c8c 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/OutputTypes/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs @@ -1,30 +1,24 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.CodeAnalysis; -using Microsoft.Generator.CSharp.Expressions; using System; using System.Collections.Generic; +using Microsoft.Generator.CSharp.Expressions; -namespace Microsoft.Generator.CSharp +namespace Microsoft.Generator.CSharp.Providers { public abstract class TypeProvider { - private readonly Lazy _existingType; - - protected readonly SourceInputModel? _sourceInputModel; protected string? _deprecated; - protected TypeProvider(SourceInputModel? sourceInputModel) - { - _sourceInputModel = sourceInputModel; - _existingType = new Lazy(() => sourceInputModel?.FindForType(Name)); - } + private string? _fileName; + + public string FileName => _fileName ??= GetFileName(); + protected virtual string GetFileName() => $"{Name}.cs"; public abstract string Name { get; } public virtual string Namespace => CodeModelPlugin.Instance.Configuration.Namespace; public virtual FormattableString Description { get; } = FormattableStringHelpers.Empty; - protected INamedTypeSymbol? ExistingType => _existingType.Value; internal virtual Type? SerializeAs => null; @@ -84,32 +78,32 @@ private TypeSignatureModifiers GetDeclarationModifiersInternal() private IReadOnlyList? _implements; public IReadOnlyList Implements => _implements ??= BuildImplements(); - private IReadOnlyList? _properties; - public IReadOnlyList Properties => _properties ??= BuildProperties(); + private IReadOnlyList? _properties; + public IReadOnlyList Properties => _properties ??= BuildProperties(); - private IReadOnlyList? _methods; - public IReadOnlyList Methods => _methods ??= BuildMethods(); + private IReadOnlyList? _methods; + public IReadOnlyList Methods => _methods ??= BuildMethods(); - private IReadOnlyList? _constructors; - public IReadOnlyList Constructors => _constructors ??= BuildConstructors(); + private IReadOnlyList? _constructors; + public IReadOnlyList Constructors => _constructors ??= BuildConstructors(); - private IReadOnlyList? _fields; - public IReadOnlyList Fields => _fields ??= BuildFields(); + private IReadOnlyList? _fields; + public IReadOnlyList Fields => _fields ??= BuildFields(); private IReadOnlyList? _nestedTypes; public IReadOnlyList NestedTypes => _nestedTypes ??= BuildNestedTypes(); protected virtual CSharpType[] BuildTypeArguments() => Array.Empty(); - protected virtual PropertyDeclaration[] BuildProperties() => Array.Empty(); + protected virtual PropertyProvider[] BuildProperties() => Array.Empty(); - protected virtual FieldDeclaration[] BuildFields() => Array.Empty(); + protected virtual FieldProvider[] BuildFields() => Array.Empty(); protected virtual CSharpType[] BuildImplements() => Array.Empty(); - protected virtual CSharpMethod[] BuildMethods() => Array.Empty(); + protected virtual MethodProvider[] BuildMethods() => Array.Empty(); - protected virtual CSharpMethod[] BuildConstructors() => Array.Empty(); + protected virtual MethodProvider[] BuildConstructors() => Array.Empty(); protected virtual TypeProvider[] BuildNestedTypes() => Array.Empty(); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Utilities/StringExtensions.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Shared/StringExtensions.cs similarity index 100% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Utilities/StringExtensions.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Shared/StringExtensions.cs diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/ExtensibleSnippets.JsonElementSnippets.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/ExtensibleSnippets.JsonElementSnippets.cs index 9930629e80b..66d95d98284 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/ExtensibleSnippets.JsonElementSnippets.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/ExtensibleSnippets.JsonElementSnippets.cs @@ -1,19 +1,22 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -namespace Microsoft.Generator.CSharp.Expressions +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Statements; + +namespace Microsoft.Generator.CSharp.Snippets { public abstract partial class ExtensibleSnippets { public abstract class JsonElementSnippets { - public abstract ValueExpression GetBytesFromBase64(JsonElementExpression element, string? format); - public abstract ValueExpression GetChar(JsonElementExpression element); - public abstract ValueExpression GetDateTimeOffset(JsonElementExpression element, string? format); - public abstract ValueExpression GetObject(JsonElementExpression element); - public abstract ValueExpression GetTimeSpan(JsonElementExpression element, string? format); + public abstract ValueExpression GetBytesFromBase64(JsonElementSnippet element, string? format); + public abstract ValueExpression GetChar(JsonElementSnippet element); + public abstract ValueExpression GetDateTimeOffset(JsonElementSnippet element, string? format); + public abstract ValueExpression GetObject(JsonElementSnippet element); + public abstract ValueExpression GetTimeSpan(JsonElementSnippet element, string? format); - public abstract MethodBodyStatement ThrowNonNullablePropertyIsNull(JsonPropertyExpression property); + public abstract MethodBodyStatement ThrowNonNullablePropertyIsNull(JsonPropertySnippet property); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/ExtensibleSnippets.ModelSnippets.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/ExtensibleSnippets.ModelSnippets.cs index b11691f8b5d..dfab083d9b2 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/ExtensibleSnippets.ModelSnippets.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/ExtensibleSnippets.ModelSnippets.cs @@ -1,13 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -namespace Microsoft.Generator.CSharp.Expressions +using Microsoft.Generator.CSharp.Providers; + +namespace Microsoft.Generator.CSharp.Snippets { public abstract partial class ExtensibleSnippets { public abstract class ModelSnippets { - public abstract CSharpMethod BuildFromOperationResponseMethod(TypeProvider typeProvider, MethodSignatureModifiers modifiers); + public abstract MethodProvider BuildFromOperationResponseMethod(TypeProvider typeProvider, MethodSignatureModifiers modifiers); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/ExtensibleSnippets.RestOperationsSnippets.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/ExtensibleSnippets.RestOperationsSnippets.cs index fd4f76cc64a..3ad6f3e2c86 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/ExtensibleSnippets.RestOperationsSnippets.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/ExtensibleSnippets.RestOperationsSnippets.cs @@ -2,21 +2,23 @@ // Licensed under the MIT License. using System; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Statements; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Snippets { public abstract partial class ExtensibleSnippets { public abstract class RestOperationsSnippets { - public abstract TypedValueExpression GetTypedResponseFromValue(TypedValueExpression value, TypedValueExpression response); - public abstract TypedValueExpression GetTypedResponseFromModel(TypeProvider typeProvider, TypedValueExpression response); - public abstract TypedValueExpression GetTypedResponseFromEnum(EnumTypeProvider enumType, TypedValueExpression response); - public abstract TypedValueExpression GetTypedResponseFromBinaryData(Type responseType, TypedValueExpression response, string? contentType = null); + public abstract TypedSnippet GetTypedResponseFromValue(TypedSnippet value, TypedSnippet response); + public abstract TypedSnippet GetTypedResponseFromModel(TypeProvider typeProvider, TypedSnippet response); + public abstract TypedSnippet GetTypedResponseFromEnum(EnumProvider enumType, TypedSnippet response); + public abstract TypedSnippet GetTypedResponseFromBinaryData(Type responseType, TypedSnippet response, string? contentType = null); - public abstract MethodBodyStatement DeclareHttpMessage(MethodSignatureBase createRequestMethodSignature, out TypedValueExpression message); - public abstract MethodBodyStatement DeclareContentWithUtf8JsonWriter(out TypedValueExpression content, out Utf8JsonWriterExpression writer); - public abstract StreamExpression GetContentStream(TypedValueExpression response); + public abstract MethodBodyStatement DeclareHttpMessage(MethodSignatureBase createRequestMethodSignature, out TypedSnippet message); + public abstract MethodBodyStatement DeclareContentWithUtf8JsonWriter(out TypedSnippet content, out Utf8JsonWriterSnippet writer); + public abstract StreamSnippet GetContentStream(TypedSnippet response); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/ExtensibleSnippets.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/ExtensibleSnippets.cs index af31598a6b5..ada1cf4c8f1 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/ExtensibleSnippets.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/ExtensibleSnippets.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -namespace Microsoft.Generator.CSharp.Expressions +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets { public abstract partial class ExtensibleSnippets { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.Argument.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.Argument.cs new file mode 100644 index 00000000000..3efd786a69c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.Argument.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Statements; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public static partial class Snippet + { + public class Argument + { + public static MethodBodyStatement AssertNotNull(ValueExpression variable) + { + return ArgumentProvider.Instance.AssertNotNull(variable); + } + + public static MethodBodyStatement AssertNotNullOrEmpty(ValueExpression variable) + { + return ArgumentProvider.Instance.AssertNotNullOrEmpty(variable); + } + + public static MethodBodyStatement AssertNotNullOrWhiteSpace(ValueExpression variable) + { + return ArgumentProvider.Instance.AssertNotNullOrWhiteSpace(variable); + } + + public static MethodBodyStatement ValidateParameter(ParameterProvider parameter) + { + return parameter.Validation switch + { + ParameterValidationType.AssertNotNullOrEmpty => AssertNotNullOrEmpty(parameter), + ParameterValidationType.AssertNotNull => AssertNotNull(parameter), + _ => EmptyStatement + }; + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippets.DeclarationStatements.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.DeclarationStatements.cs similarity index 50% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippets.DeclarationStatements.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.DeclarationStatements.cs index 334baf861ca..64c89c98803 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippets.DeclarationStatements.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.DeclarationStatements.cs @@ -2,112 +2,114 @@ // Licensed under the MIT License. using System; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Statements; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Snippets { - public static partial class Snippets + public static partial class Snippet { - public static MethodBodyStatement UsingDeclare(string name, CSharpType type, ValueExpression value, out VariableReference variable) + public static MethodBodyStatement UsingDeclare(string name, CSharpType type, ValueExpression value, out VariableReferenceSnippet variable) { var declaration = new CodeWriterDeclaration(name); - variable = new VariableReference(type, declaration); + variable = new VariableReferenceSnippet(type, declaration); return new UsingDeclareVariableStatement(type, declaration, value); } - public static MethodBodyStatement UsingDeclare(string name, JsonDocumentExpression value, out JsonDocumentExpression variable) - => UsingDeclare(name, value, d => new JsonDocumentExpression(d), out variable); + public static MethodBodyStatement UsingDeclare(string name, JsonDocumentSnippet value, out JsonDocumentSnippet variable) + => UsingDeclare(name, value, d => new JsonDocumentSnippet(d), out variable); - public static MethodBodyStatement UsingDeclare(string name, StreamExpression value, out StreamExpression variable) - => UsingDeclare(name, value, d => new StreamExpression(d), out variable); + public static MethodBodyStatement UsingDeclare(string name, StreamSnippet value, out StreamSnippet variable) + => UsingDeclare(name, value, d => new StreamSnippet(d), out variable); - public static MethodBodyStatement UsingDeclare(VariableReference variable, ValueExpression value) + public static MethodBodyStatement UsingDeclare(VariableReferenceSnippet variable, ValueExpression value) => new UsingDeclareVariableStatement(variable.Type, variable.Declaration, value); - public static MethodBodyStatement Declare(CSharpType variableType, string name, ValueExpression value, out TypedValueExpression variable) + public static MethodBodyStatement Declare(CSharpType variableType, string name, ValueExpression value, out TypedSnippet variable) { - var variableRef = new VariableReference(variableType, name); + var variableRef = new VariableReferenceSnippet(variableType, name); variable = variableRef; return Declare(variableRef, value); } - public static MethodBodyStatement Declare(string name, BinaryDataExpression value, out BinaryDataExpression variable) - => Declare(name, value, d => new BinaryDataExpression(d), out variable); + public static MethodBodyStatement Declare(string name, BinaryDataSnippet value, out BinaryDataSnippet variable) + => Declare(name, value, d => new BinaryDataSnippet(d), out variable); - public static MethodBodyStatement Declare(string name, DictionaryExpression value, out DictionaryExpression variable) - => Declare(name, value, d => new DictionaryExpression(value.KeyType, value.ValueType, d), out variable); + public static MethodBodyStatement Declare(string name, DictionarySnippet value, out DictionarySnippet variable) + => Declare(name, value, d => new DictionarySnippet(value.KeyType, value.ValueType, d), out variable); - public static MethodBodyStatement Declare(string name, EnumerableExpression value, out EnumerableExpression variable) - => Declare(name, value, d => new EnumerableExpression(value.ItemType, d), out variable); + public static MethodBodyStatement Declare(string name, EnumerableSnippet value, out EnumerableSnippet variable) + => Declare(name, value, d => new EnumerableSnippet(value.ItemType, d), out variable); - public static MethodBodyStatement Declare(string name, JsonElementExpression value, out JsonElementExpression variable) - => Declare(name, value, d => new JsonElementExpression(d), out variable); + public static MethodBodyStatement Declare(string name, JsonElementSnippet value, out JsonElementSnippet variable) + => Declare(name, value, d => new JsonElementSnippet(d), out variable); - public static MethodBodyStatement Declare(string name, ListExpression value, out ListExpression variable) - => Declare(name, value, d => new ListExpression(value.ItemType, d), out variable); + public static MethodBodyStatement Declare(string name, ListSnippet value, out ListSnippet variable) + => Declare(name, value, d => new ListSnippet(value.ItemType, d), out variable); - public static MethodBodyStatement Declare(string name, StreamReaderExpression value, out StreamReaderExpression variable) - => Declare(name, value, d => new StreamReaderExpression(d), out variable); + public static MethodBodyStatement Declare(string name, StreamReaderSnippet value, out StreamReaderSnippet variable) + => Declare(name, value, d => new StreamReaderSnippet(d), out variable); - public static MethodBodyStatement Declare(string name, TypedValueExpression value, out TypedValueExpression variable) + public static MethodBodyStatement Declare(string name, TypedSnippet value, out TypedSnippet variable) { - var declaration = new VariableReference(value.Type, name); + var declaration = new VariableReferenceSnippet(value.Type, name); variable = declaration; return Declare(declaration, value); } - public static MethodBodyStatement Declare(VariableReference variable, ValueExpression value) + public static MethodBodyStatement Declare(VariableReferenceSnippet variable, ValueExpression value) => new DeclareVariableStatement(variable.Type, variable.Declaration, value); - public static MethodBodyStatement UsingVar(string name, JsonDocumentExpression value, out JsonDocumentExpression variable) - => UsingVar(name, value, d => new JsonDocumentExpression(d), out variable); + public static MethodBodyStatement UsingVar(string name, JsonDocumentSnippet value, out JsonDocumentSnippet variable) + => UsingVar(name, value, d => new JsonDocumentSnippet(d), out variable); - public static MethodBodyStatement Var(string name, DictionaryExpression value, out DictionaryExpression variable) - => Var(name, value, d => new DictionaryExpression(value.KeyType, value.ValueType, d), out variable); + public static MethodBodyStatement Var(string name, DictionarySnippet value, out DictionarySnippet variable) + => Var(name, value, d => new DictionarySnippet(value.KeyType, value.ValueType, d), out variable); - public static MethodBodyStatement Var(string name, ListExpression value, out ListExpression variable) - => Var(name, value, d => new ListExpression(value.ItemType, d), out variable); + public static MethodBodyStatement Var(string name, ListSnippet value, out ListSnippet variable) + => Var(name, value, d => new ListSnippet(value.ItemType, d), out variable); - public static MethodBodyStatement Var(string name, StringExpression value, out StringExpression variable) - => Var(name, value, d => new StringExpression(d), out variable); + public static MethodBodyStatement Var(string name, StringSnippet value, out StringSnippet variable) + => Var(name, value, d => new StringSnippet(d), out variable); - public static MethodBodyStatement Var(string name, Utf8JsonWriterExpression value, out Utf8JsonWriterExpression variable) - => Var(name, value, d => new Utf8JsonWriterExpression(d), out variable); + public static MethodBodyStatement Var(string name, Utf8JsonWriterSnippet value, out Utf8JsonWriterSnippet variable) + => Var(name, value, d => new Utf8JsonWriterSnippet(d), out variable); - public static MethodBodyStatement Var(string name, TypedValueExpression value, out TypedValueExpression variable) + public static MethodBodyStatement Var(string name, TypedSnippet value, out TypedSnippet variable) { - var reference = new VariableReference(value.Type, name); + var reference = new VariableReferenceSnippet(value.Type, name); variable = reference; return Var(reference, value); } - public static MethodBodyStatement Var(VariableReference variable, ValueExpression value) + public static MethodBodyStatement Var(VariableReferenceSnippet variable, ValueExpression value) => new DeclareVariableStatement(null, variable.Declaration, value); - private static MethodBodyStatement UsingDeclare(string name, T value, Func factory, out T variable) where T : TypedValueExpression + private static MethodBodyStatement UsingDeclare(string name, T value, Func factory, out T variable) where T : TypedSnippet { var declaration = new CodeWriterDeclaration(name); - variable = factory(new VariableReference(value.Type, declaration)); + variable = factory(new VariableReferenceSnippet(value.Type, declaration)); return new UsingDeclareVariableStatement(value.Type, declaration, value); } - private static MethodBodyStatement UsingVar(string name, T value, Func factory, out T variable) where T : TypedValueExpression + private static MethodBodyStatement UsingVar(string name, T value, Func factory, out T variable) where T : TypedSnippet { var declaration = new CodeWriterDeclaration(name); - variable = factory(new VariableReference(value.Type, declaration)); + variable = factory(new VariableReferenceSnippet(value.Type, declaration)); return new UsingDeclareVariableStatement(null, declaration, value); } - private static MethodBodyStatement Declare(string name, T value, Func factory, out T variable) where T : TypedValueExpression + private static MethodBodyStatement Declare(string name, T value, Func factory, out T variable) where T : TypedSnippet { var declaration = new CodeWriterDeclaration(name); - variable = factory(new VariableReference(value.Type, declaration)); + variable = factory(new VariableReferenceSnippet(value.Type, declaration)); return new DeclareVariableStatement(value.Type, declaration, value); } - private static MethodBodyStatement Var(string name, T value, Func factory, out T variable) where T : TypedValueExpression + private static MethodBodyStatement Var(string name, T value, Func factory, out T variable) where T : TypedSnippet { var declaration = new CodeWriterDeclaration(name); - variable = factory(new VariableReference(value.Type, declaration)); + variable = factory(new VariableReferenceSnippet(value.Type, declaration)); return new DeclareVariableStatement(null, declaration, value); } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.JsonSerializer.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.JsonSerializer.cs new file mode 100644 index 00000000000..a3dea28d304 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.JsonSerializer.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text.Json; +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public static partial class Snippet + { + + public static class JsonSerializer + { + public static InvokeStaticMethodExpression Serialize(ValueExpression writer, ValueExpression value, ValueExpression? options = null) + { + var arguments = options is null + ? new[] { writer, value } + : new[] { writer, value, options }; + return new InvokeStaticMethodExpression(typeof(JsonSerializer), nameof(JsonSerializer.Serialize), arguments); + } + + public static InvokeStaticMethodExpression Deserialize(JsonElementSnippet element, CSharpType serializationType, ValueExpression? options = null) + { + ValueExpression[] arguments = options is null + ? [element.GetRawText()] + : new[] { element.GetRawText(), options }; + return new InvokeStaticMethodExpression(typeof(JsonSerializer), nameof(JsonSerializer.Deserialize), arguments, new[] { serializationType }); + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippets.Linq.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.Linq.cs similarity index 66% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippets.Linq.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.Linq.cs index 4b73523b8a0..b283a80f6f6 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippets.Linq.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.Linq.cs @@ -1,11 +1,12 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Linq; +using Microsoft.Generator.CSharp.Expressions; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Snippets { - public static partial class Snippets + public static partial class Snippet { public static class Linq { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippets.New.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.New.cs similarity index 60% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippets.New.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.New.cs index a64ec36bef1..1c7d00f29aa 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippets.New.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.New.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -6,14 +6,16 @@ using System.IO; using System.Linq; using System.Text.Json; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Providers; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Snippets { - public static partial class Snippets + public static partial class Snippet { public static class New { - public static ValueExpression ArgumentOutOfRangeException(EnumTypeProvider enumType, Parameter valueParameter) + public static ValueExpression ArgumentOutOfRangeException(EnumProvider enumType, ParameterProvider valueParameter) => Instance(typeof(ArgumentOutOfRangeException), Nameof(valueParameter), valueParameter, Literal($"Unknown {enumType.Name} value.")); public static ValueExpression ArgumentOutOfRangeException(ValueExpression valueParameter, string message, bool wrapInNameOf = true) => Instance(typeof(ArgumentOutOfRangeException), wrapInNameOf ? Nameof(valueParameter) : valueParameter, Literal(message)); @@ -39,24 +41,24 @@ public static ValueExpression ArgumentException(ValueExpression parameter, Value public static ValueExpression JsonException(ValueExpression message) => Instance(typeof(JsonException), message); - public static EnumerableExpression Array(CSharpType? elementType) => new(elementType ?? typeof(object), new NewArrayExpression(elementType)); - public static EnumerableExpression Array(CSharpType? elementType, params ValueExpression[] items) => new(elementType ?? typeof(object), new NewArrayExpression(elementType, new ArrayInitializerExpression(items))); - public static EnumerableExpression Array(CSharpType? elementType, bool isInline, params ValueExpression[] items) => new(elementType ?? typeof(object), new NewArrayExpression(elementType, new ArrayInitializerExpression(items, isInline))); - public static EnumerableExpression Array(CSharpType? elementType, ValueExpression size) => new(elementType ?? typeof(object), new NewArrayExpression(elementType, Size: size)); + public static EnumerableSnippet Array(CSharpType? elementType) => new(elementType ?? typeof(object), new NewArrayExpression(elementType)); + public static EnumerableSnippet Array(CSharpType? elementType, params ValueExpression[] items) => new(elementType ?? typeof(object), new NewArrayExpression(elementType, new ArrayInitializerExpression(items))); + public static EnumerableSnippet Array(CSharpType? elementType, bool isInline, params ValueExpression[] items) => new(elementType ?? typeof(object), new NewArrayExpression(elementType, new ArrayInitializerExpression(items, isInline))); + public static EnumerableSnippet Array(CSharpType? elementType, ValueExpression size) => new(elementType ?? typeof(object), new NewArrayExpression(elementType, Size: size)); - public static DictionaryExpression Dictionary(CSharpType keyType, CSharpType valueType) + public static DictionarySnippet Dictionary(CSharpType keyType, CSharpType valueType) => new(keyType, valueType, new NewDictionaryExpression(new CSharpType(typeof(Dictionary<,>), keyType, valueType))); - public static DictionaryExpression Dictionary(CSharpType keyType, CSharpType valueType, params (ValueExpression Key, ValueExpression Value)[] values) + public static DictionarySnippet Dictionary(CSharpType keyType, CSharpType valueType, params (ValueExpression Key, ValueExpression Value)[] values) => new(keyType, valueType, new NewDictionaryExpression(new CSharpType(typeof(Dictionary<,>), keyType, valueType), new DictionaryInitializerExpression(values))); - public static TypedValueExpression JsonSerializerOptions() => new FrameworkTypeExpression(typeof(JsonSerializerOptions), new NewJsonSerializerOptionsExpression()); + public static TypedSnippet JsonSerializerOptions() => new FrameworkTypeSnippet(typeof(JsonSerializerOptions), new ValueExpression()); - public static ListExpression List(CSharpType elementType) => new(elementType, Instance(new CSharpType(typeof(List<>), elementType))); + public static ListSnippet List(CSharpType elementType) => new(elementType, Instance(new CSharpType(typeof(List<>), elementType))); - public static StreamReaderExpression StreamReader(ValueExpression stream) => new(Instance(typeof(StreamReader), stream)); + public static StreamReaderSnippet StreamReader(ValueExpression stream) => new(Instance(typeof(StreamReader), stream)); - public static TimeSpanExpression TimeSpan(int hours, int minutes, int seconds) => new(Instance(typeof(TimeSpan), Int(hours), Int(minutes), Int(seconds))); - public static TypedValueExpression Uri(string uri) => Instance(typeof(Uri), Literal(uri)); + public static TimeSpanSnippet TimeSpan(int hours, int minutes, int seconds) => new(Instance(typeof(TimeSpan), Int(hours), Int(minutes), Int(seconds))); + public static TypedSnippet Uri(string uri) => Instance(typeof(Uri), Literal(uri)); public static ValueExpression Anonymous(string key, ValueExpression value) => Anonymous(new Dictionary { [key] = value }); public static ValueExpression Anonymous(IReadOnlyDictionary? properties) => new KeywordExpression("new", new ObjectInitializerExpression(properties, UseSingleLine: false)); @@ -65,8 +67,8 @@ public static DictionaryExpression Dictionary(CSharpType keyType, CSharpType val public static ValueExpression Instance(CSharpType type, IReadOnlyList arguments) => new NewInstanceExpression(type, arguments); public static ValueExpression Instance(CSharpType type, params ValueExpression[] arguments) => new NewInstanceExpression(type, arguments); public static ValueExpression Instance(CSharpType type, IReadOnlyDictionary properties) => new NewInstanceExpression(type, System.Array.Empty(), new ObjectInitializerExpression(properties)); - public static TypedValueExpression Instance(Type type, params ValueExpression[] arguments) => new FrameworkTypeExpression(type, new NewInstanceExpression(type, arguments)); - public static TypedValueExpression Instance(Type type, IReadOnlyDictionary properties) => new FrameworkTypeExpression(type, new NewInstanceExpression(type, System.Array.Empty(), new ObjectInitializerExpression(properties))); + public static TypedSnippet Instance(Type type, params ValueExpression[] arguments) => new FrameworkTypeSnippet(type, new NewInstanceExpression(type, arguments)); + public static TypedSnippet Instance(Type type, IReadOnlyDictionary properties) => new FrameworkTypeSnippet(type, new NewInstanceExpression(type, System.Array.Empty(), new ObjectInitializerExpression(properties))); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.Where.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.Where.cs new file mode 100644 index 00000000000..828f6ca77a9 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.Where.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public static partial class Snippet + { + public static class Where + { + public static WhereExpression NotNull(CSharpType type) => new WhereExpression(type, new KeywordExpression("notnull", null)); + public static WhereExpression Struct(CSharpType type) => new WhereExpression(type, new KeywordExpression("struct", null)); + public static WhereExpression Class(CSharpType type) => new WhereExpression(type, new KeywordExpression("class", null)); + public static WhereExpression Implements(CSharpType type, params CSharpType[] typesToImplement) + { + if (typesToImplement.Length == 0) + { + throw new InvalidOperationException("You must provide at least one type to implement"); + } + List constraints = new List(); + foreach (var implementation in typesToImplement) + { + constraints.Add(implementation); + } + return new WhereExpression(type, constraints); + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippets.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.cs similarity index 58% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippets.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.cs index 2a7b760bfb7..991a2391cd3 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippets.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippet.cs @@ -4,10 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Statements; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Snippets { - public static partial class Snippets + public static partial class Snippet { public static ExtensibleSnippets Extensible => CodeModelPlugin.Instance.ExtensibleSnippets; public static MethodBodyStatement EmptyStatement { get; } = new(); @@ -20,14 +22,14 @@ public static partial class Snippets public static ValueExpression Default { get; } = new KeywordExpression("default", null); public static ValueExpression Null { get; } = new KeywordExpression("null", null); public static ValueExpression This { get; } = new KeywordExpression("this", null); - public static BoolExpression True { get; } = new(new KeywordExpression("true", null)); - public static BoolExpression False { get; } = new(new KeywordExpression("false", null)); + public static BoolSnippet True { get; } = new(new KeywordExpression("true", null)); + public static BoolSnippet False { get; } = new(new KeywordExpression("false", null)); - public static BoolExpression Bool(bool value) => value ? True : False; - public static IntExpression Int(int value) => new(Literal(value)); - public static LongExpression Long(long value) => new(Literal(value)); - public static ValueExpression Float(float value) => new FormattableStringToExpression($"{value}f"); - public static ValueExpression Double(double value) => new FormattableStringToExpression($"{value}d"); + public static BoolSnippet Bool(bool value) => value ? True : False; + public static IntSnippet Int(int value) => new(Literal(value)); + public static LongSnippet Long(long value) => new(Literal(value)); + public static ValueExpression Float(float value) => Literal(value); + public static ValueExpression Double(double value) => Literal(value); public static ValueExpression Nameof(ValueExpression expression) => new InvokeInstanceMethodExpression(null, "nameof", new[] { expression }, null, false); public static ValueExpression ThrowExpression(ValueExpression expression) => new KeywordExpression("throw", expression); @@ -35,36 +37,35 @@ public static partial class Snippets public static ValueExpression NullCoalescing(ValueExpression left, ValueExpression right) => new BinaryOperatorExpression("??", left, right); // TO-DO: Migrate remaining class as part of output classes migration : https://github.com/Azure/autorest.csharp/issues/4198 //public static ValueExpression EnumValue(EnumType type, EnumTypeValue value) => new MemberExpression(new TypeReference(type.Type), value.Declaration.Name); - public static ValueExpression FrameworkEnumValue(TEnum value) where TEnum : struct, Enum => new MemberExpression(new TypeReference(typeof(TEnum)), Enum.GetName(value)!); + public static ValueExpression FrameworkEnumValue(TEnum value) where TEnum : struct, Enum => new MemberExpression(new TypeReferenceExpression(typeof(TEnum)), Enum.GetName(value)!); public static ValueExpression RemoveAllNullConditional(ValueExpression expression) => expression switch { NullConditionalExpression nullConditional => RemoveAllNullConditional(nullConditional.Inner), MemberExpression { Inner: { } inner } member => member with { Inner = RemoveAllNullConditional(inner) }, - TypedValueExpression typed => typed with { Untyped = RemoveAllNullConditional(typed.Untyped) }, _ => expression }; - public static TypedValueExpression RemoveAllNullConditional(TypedValueExpression expression) + public static TypedSnippet RemoveAllNullConditional(TypedSnippet expression) => expression with { Untyped = RemoveAllNullConditional(expression.Untyped) }; public static ValueExpression Literal(object? value) => new LiteralExpression(value); - public static StringExpression Literal(string? value) => new(value is null ? Null : new StringLiteralExpression(value, false)); - public static StringExpression LiteralU8(string value) => new(new StringLiteralExpression(value, true)); + public static StringSnippet Literal(string? value) => new(value is null ? Null : new LiteralExpression(value)); + public static StringSnippet LiteralU8(string value) => new(new UnaryOperatorExpression("u8", new LiteralExpression(value), true)); - public static BoolExpression GreaterThan(ValueExpression left, ValueExpression right) => new(new BinaryOperatorExpression(">", left, right)); - public static BoolExpression LessThan(ValueExpression left, ValueExpression right) => new(new BinaryOperatorExpression("<", left, right)); - public static BoolExpression Equal(ValueExpression left, ValueExpression right) => new(new BinaryOperatorExpression("==", left, right)); - public static BoolExpression NotEqual(ValueExpression left, ValueExpression right) => new(new BinaryOperatorExpression("!=", left, right)); + public static BoolSnippet GreaterThan(ValueExpression left, ValueExpression right) => new(new BinaryOperatorExpression(">", left, right)); + public static BoolSnippet LessThan(ValueExpression left, ValueExpression right) => new(new BinaryOperatorExpression("<", left, right)); + public static BoolSnippet Equal(ValueExpression left, ValueExpression right) => new(new BinaryOperatorExpression("==", left, right)); + public static BoolSnippet NotEqual(ValueExpression left, ValueExpression right) => new(new BinaryOperatorExpression("!=", left, right)); - public static BoolExpression Is(ValueExpression left, ValueExpression right) + public static BoolSnippet Is(ValueExpression left, ValueExpression right) => new(new BinaryOperatorExpression("is", left, right)); - public static BoolExpression Or(BoolExpression left, BoolExpression right) => new(new BinaryOperatorExpression("||", left.Untyped, right.Untyped)); - public static BoolExpression And(BoolExpression left, BoolExpression right) => new(new BinaryOperatorExpression("&&", left.Untyped, right.Untyped)); - public static BoolExpression Not(BoolExpression operand) => new(new UnaryOperatorExpression("!", operand, false)); + public static BoolSnippet Or(BoolSnippet left, BoolSnippet right) => new(new BinaryOperatorExpression("||", left.Untyped, right.Untyped)); + public static BoolSnippet And(BoolSnippet left, BoolSnippet right) => new(new BinaryOperatorExpression("&&", left.Untyped, right.Untyped)); + public static BoolSnippet Not(BoolSnippet operand) => new(new UnaryOperatorExpression("!", operand, false)); public static MethodBodyStatement EmptyLine => new EmptyLineStatement(); public static KeywordStatement Continue => new("continue", null); @@ -73,22 +74,22 @@ public static BoolExpression Is(ValueExpression left, ValueExpression right) public static KeywordStatement Return() => new("return", null); public static KeywordStatement Throw(ValueExpression expression) => new("throw", expression); - public static EnumerableExpression InvokeArrayEmpty(CSharpType arrayItemType) + public static EnumerableSnippet InvokeArrayEmpty(CSharpType arrayItemType) => new(arrayItemType, new InvokeStaticMethodExpression(typeof(Array), nameof(Array.Empty), Array.Empty(), new[] { arrayItemType })); - public static StreamExpression InvokeFileOpenRead(string filePath) - => new(new InvokeStaticMethodExpression(typeof(System.IO.File), nameof(System.IO.File.OpenRead), new[] { Literal(filePath) })); - public static StreamExpression InvokeFileOpenWrite(string filePath) - => new(new InvokeStaticMethodExpression(typeof(System.IO.File), nameof(System.IO.File.OpenWrite), new[] { Literal(filePath) })); + public static StreamSnippet InvokeFileOpenRead(string filePath) + => new(new InvokeStaticMethodExpression(typeof(System.IO.File), nameof(System.IO.File.OpenRead), [Literal(filePath)])); + public static StreamSnippet InvokeFileOpenWrite(string filePath) + => new(new InvokeStaticMethodExpression(typeof(System.IO.File), nameof(System.IO.File.OpenWrite), [Literal(filePath)])); - public static MethodBodyStatement InvokeCustomSerializationMethod(string methodName, Utf8JsonWriterExpression utf8JsonWriter) + public static MethodBodyStatement InvokeCustomSerializationMethod(string methodName, Utf8JsonWriterSnippet utf8JsonWriter) => new InvokeInstanceMethodStatement(null, methodName, utf8JsonWriter); - public static MethodBodyStatement InvokeCustomBicepSerializationMethod(string methodName, StringBuilderExpression stringBuilder) + public static MethodBodyStatement InvokeCustomBicepSerializationMethod(string methodName, StringBuilderSnippet stringBuilder) => new InvokeInstanceMethodStatement(null, methodName, stringBuilder); - public static MethodBodyStatement InvokeCustomDeserializationMethod(string methodName, JsonPropertyExpression jsonProperty, CodeWriterDeclaration variable) - => new InvokeStaticMethodStatement(null, methodName, new ValueExpression[] { jsonProperty, new FormattableStringToExpression($"ref {variable}") }); + public static MethodBodyStatement InvokeCustomDeserializationMethod(string methodName, JsonPropertySnippet jsonProperty, VariableReferenceSnippet variable) + => new InvokeStaticMethodStatement(null, methodName, new ValueExpression[] { jsonProperty, new KeywordExpression("ref", variable) }); public static AssignValueIfNullStatement AssignIfNull(ValueExpression variable, ValueExpression expression) => new(variable, expression); public static AssignValueStatement Assign(ValueExpression variable, ValueExpression expression) => new(variable, expression); @@ -99,11 +100,12 @@ public static MethodBodyStatement AssignOrReturn(ValueExpression? variable, Valu public static MethodBodyStatement InvokeConsoleWriteLine(ValueExpression expression) => new InvokeStaticMethodStatement(typeof(Console), nameof(Console.WriteLine), expression); - private static BoolExpression Is(T value, string name, Func factory, out T variable) where T : TypedValueExpression + private static BoolSnippet Is(T value, string name, Func factory, out T variable) where T : TypedSnippet { var declaration = new CodeWriterDeclaration(name); - variable = factory(new VariableReference(value.Type, declaration)); - return new(new BinaryOperatorExpression("is", value, new FormattableStringToExpression($"{value.Type} {declaration:D}"))); + var variableRef = new VariableReferenceSnippet(value.Type, declaration); + variable = factory(variableRef); + return new(new BinaryOperatorExpression("is", value, new DeclarationExpression(variableRef.Type, variableRef.Declaration, false))); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippets.Argument.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippets.Argument.cs deleted file mode 100644 index ead6c30dd1d..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Snippets/Snippets.Argument.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections.Generic; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public static partial class Snippets - { - public class Argument - { - public static MethodBodyStatement AssertNotNull(ValueExpression variable) - { - return new IfStatement(Equal(variable, Null)) - { - new ThrowStatement(ThrowExpression(New.ArgumentNullException(variable))) - }; - } - - public static MethodBodyStatement AssertNotNullOrEmpty(ValueExpression variable) - { - return new List() - { - AssertNotNull(variable), - new IfStatement(Equal(new MemberExpression(variable, "Length"), Literal(0))) - { - new ThrowStatement(ThrowExpression(New.ArgumentException(variable, string.Empty))) - } - }; - } - - public static MethodBodyStatement ValidateParameter(Parameter parameter) - { - return parameter.Validation switch - { - ValidationType.AssertNotNullOrEmpty => AssertNotNullOrEmpty(parameter), - ValidationType.AssertNotNull => AssertNotNull(parameter), - _ => EmptyStatement - }; - } - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ClientSourceInput.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ClientSourceInput.cs index f81e9360d2c..481c8f75d3e 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ClientSourceInput.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ClientSourceInput.cs @@ -3,7 +3,7 @@ using Microsoft.CodeAnalysis; -namespace Microsoft.Generator.CSharp +namespace Microsoft.Generator.CSharp.SourceInput { internal record ClientSourceInput(INamedTypeSymbol? ParentClientType); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/CodeGenAttributes.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/CodeGenAttributes.cs index 50d189308cb..f522118bd93 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/CodeGenAttributes.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/CodeGenAttributes.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections.Generic; @@ -8,7 +8,7 @@ using Microsoft.CodeAnalysis; using Microsoft.Generator.CSharp.Customization; -namespace Microsoft.Generator.CSharp +namespace Microsoft.Generator.CSharp.SourceInput { internal class CodeGenAttributes { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/CompilationCustomCode.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/CompilationCustomCode.cs index 3b4b87e7180..665af0c323b 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/CompilationCustomCode.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/CompilationCustomCode.cs @@ -1,12 +1,12 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections.Generic; using Microsoft.CodeAnalysis; -namespace Microsoft.Generator.CSharp +namespace Microsoft.Generator.CSharp.SourceInput { - public abstract class CompilationCustomCode + internal abstract class CompilationCustomCode { protected Compilation _compilation; diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ModelTypeMapping.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ModelTypeMapping.cs index 5f0e1a7cc4e..dbf8f4abef4 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ModelTypeMapping.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ModelTypeMapping.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using Microsoft.CodeAnalysis; -namespace Microsoft.Generator.CSharp +namespace Microsoft.Generator.CSharp.SourceInput { internal class ModelTypeMapping { @@ -67,7 +67,7 @@ private static bool ShouldIncludeMember(ISymbol member) renamedSymbol : _propertyMappings.TryGetValue(name, out var memberSymbol) ? memberSymbol : null; - public SourcePropertySerializationMapping? GetForMemberSerialization(string name) + internal SourcePropertySerializationMapping? GetForMemberSerialization(string name) => _typeSerializationMappings.TryGetValue(name, out var serialization) ? serialization : null; public IEnumerable GetPropertiesWithSerialization() diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ProtocolCompilationCustomCode.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ProtocolCompilationCustomCode.cs index 2d225dc6873..d5487939424 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ProtocolCompilationCustomCode.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ProtocolCompilationCustomCode.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections.Generic; @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; -namespace Microsoft.Generator.CSharp +namespace Microsoft.Generator.CSharp.SourceInput { internal class ProtocolCompilationCustomCode : CompilationCustomCode { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/SourceInputHelper.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/SourceInputHelper.cs index 4750b226ffc..102abb665a4 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/SourceInputHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/SourceInputHelper.cs @@ -1,12 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using Microsoft.CodeAnalysis; +using Microsoft.Generator.CSharp.Providers; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -namespace Microsoft.Generator.CSharp +namespace Microsoft.Generator.CSharp.SourceInput { internal static class SourceInputHelper { @@ -72,7 +73,7 @@ internal static bool TryGetExistingMethod(INamedTypeSymbol? type, MethodSignatur return false; } - private static bool TypesAreEqual(ImmutableArray candidateParameters, IReadOnlyList signatureParameters) + private static bool TypesAreEqual(ImmutableArray candidateParameters, IReadOnlyList signatureParameters) { if (candidateParameters.Length != signatureParameters.Count) { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/SourceInputModel.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/SourceInputModel.cs index 6537541a416..327e56c017d 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/SourceInputModel.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/SourceInputModel.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -10,13 +10,21 @@ using Microsoft.Build.Construction; using Microsoft.CodeAnalysis; using Microsoft.Generator.CSharp.Customization; -using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Providers; using NuGet.Configuration; -namespace Microsoft.Generator.CSharp +namespace Microsoft.Generator.CSharp.SourceInput { - public sealed class SourceInputModel + internal sealed class SourceInputModel { + private static SourceInputModel? _instance; + public static SourceInputModel Instance => _instance ?? throw new InvalidOperationException("SourceInputModel has not been initialized"); + + public static void Initialize(Compilation customization, CompilationCustomCode? existingCompilation = null) + { + _instance = new SourceInputModel(customization, existingCompilation); + } + private readonly CompilationCustomCode? _existingCompilation; private readonly CodeGenAttributes _codeGenAttributes; private readonly Dictionary _nameMap = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -24,7 +32,7 @@ public sealed class SourceInputModel public Compilation Customization { get; } public Compilation? PreviousContract { get; } - public SourceInputModel(Compilation customization, CompilationCustomCode? existingCompilation = null) + private SourceInputModel(Compilation customization, CompilationCustomCode? existingCompilation = null) { Customization = customization; PreviousContract = LoadBaselineContract().GetAwaiter().GetResult(); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/SourcePropertySerializationMapping.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/SourcePropertySerializationMapping.cs index e4ba41fddf2..c044be16d95 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/SourcePropertySerializationMapping.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/SourcePropertySerializationMapping.cs @@ -1,11 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections.Generic; -namespace Microsoft.Generator.CSharp +namespace Microsoft.Generator.CSharp.SourceInput { - public sealed class SourcePropertySerializationMapping + internal sealed class SourcePropertySerializationMapping { public SourcePropertySerializationMapping(string propertyName, IReadOnlyList? serializationPath, string? jsonSerializationValueHook, string? jsonDeserializationValueHook, string? bicepSerializationValueHook) { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/AssignValueIfNullStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/AssignValueIfNullStatement.cs index 567a40e13a5..1ea8b33a16e 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/AssignValueIfNullStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/AssignValueIfNullStatement.cs @@ -1,11 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -namespace Microsoft.Generator.CSharp.Expressions +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Statements { public sealed record AssignValueIfNullStatement(ValueExpression To, ValueExpression From) : MethodBodyStatement { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { To.Write(writer); writer.AppendRaw(" ??= "); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/AssignValueStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/AssignValueStatement.cs index 934e3c7611f..8ccd3ed39ad 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/AssignValueStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/AssignValueStatement.cs @@ -1,11 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -namespace Microsoft.Generator.CSharp.Expressions +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Statements { public sealed record AssignValueStatement(ValueExpression To, ValueExpression From) : MethodBodyStatement { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { To.Write(writer); writer.AppendRaw(" = "); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/AttributeStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/AttributeStatement.cs new file mode 100644 index 00000000000..7bb0adcf6fd --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/AttributeStatement.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Statements +{ + public sealed record AttributeStatement(CSharpType Type, IReadOnlyList Arguments) : MethodBodyStatement + { + public AttributeStatement(CSharpType type, params ValueExpression[] arguments) : this(type, (IReadOnlyList)arguments) { } + + internal override void Write(CodeWriter writer) + { + if (Arguments.Any()) + { + writer.Append($"[{Type}("); + foreach (var argument in Arguments) + { + argument.Write(writer); + } + writer.WriteRawLine(")]"); + } + else + { + writer.WriteLine($"[{Type}]"); + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/DeclareLocalFunctionStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/DeclareLocalFunctionStatement.cs index 4b91d3337e1..c6c1c718ee2 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/DeclareLocalFunctionStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/DeclareLocalFunctionStatement.cs @@ -1,28 +1,30 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; using System.Collections.Generic; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Providers; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Statements { - public sealed record DeclareLocalFunctionStatement(CodeWriterDeclaration Name, IReadOnlyList Parameters, CSharpType ReturnType, ValueExpression? BodyExpression, MethodBodyStatement? BodyStatement) : MethodBodyStatement + public sealed record DeclareLocalFunctionStatement(CodeWriterDeclaration Name, IReadOnlyList Parameters, CSharpType ReturnType, ValueExpression? BodyExpression, MethodBodyStatement? BodyStatement) : MethodBodyStatement { - internal DeclareLocalFunctionStatement(CodeWriterDeclaration Name, IReadOnlyList Parameters, CSharpType ReturnType, MethodBodyStatement BodyStatement) + internal DeclareLocalFunctionStatement(CodeWriterDeclaration Name, IReadOnlyList Parameters, CSharpType ReturnType, MethodBodyStatement BodyStatement) : this(Name, Parameters, ReturnType, null, BodyStatement) { } - internal DeclareLocalFunctionStatement(CodeWriterDeclaration Name, IReadOnlyList Parameters, CSharpType ReturnType, ValueExpression BodyExpression) + internal DeclareLocalFunctionStatement(CodeWriterDeclaration Name, IReadOnlyList Parameters, CSharpType ReturnType, ValueExpression BodyExpression) : this(Name, Parameters, ReturnType, BodyExpression, null) { } - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { writer.Append($"{ReturnType} {Name:D}("); - foreach (var parameter in Parameters) + for (int i = 0; i < Parameters.Count; i++) { - writer.Append($"{parameter.Type} {parameter.Name}, "); + writer.Append($"{Parameters[i].Type} {Parameters[i].Name}, "); + if (i < Parameters.Count - 1) + writer.AppendRaw(", "); } - - writer.RemoveTrailingComma(); writer.AppendRaw(")"); if (BodyExpression is not null) { @@ -32,6 +34,7 @@ public override void Write(CodeWriter writer) } else if (BodyStatement is not null) { + writer.WriteLine(); using (writer.Scope()) { BodyStatement.Write(writer); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/DeclareVariableStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/DeclareVariableStatement.cs index b52288741d7..deb33d6fc7d 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/DeclareVariableStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/DeclareVariableStatement.cs @@ -1,11 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -namespace Microsoft.Generator.CSharp.Expressions +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Statements { public sealed record DeclareVariableStatement(CSharpType? Type, CodeWriterDeclaration Name, ValueExpression Value) : MethodBodyStatement { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { if (Type != null) { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/DiagnosticScopeMethodBodyBlock.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/DiagnosticScopeMethodBodyBlock.cs deleted file mode 100644 index 27345fe4406..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/DiagnosticScopeMethodBodyBlock.cs +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record DiagnosticScopeMethodBodyBlock(Diagnostic Diagnostic, ValueExpression ClientDiagnosticsReference, MethodBodyStatement InnerStatement) : MethodBodyStatement; -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/EmptyLineStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/EmptyLineStatement.cs index ef2f7f806e3..eaf525b15dd 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/EmptyLineStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/EmptyLineStatement.cs @@ -1,11 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Statements { public sealed record EmptyLineStatement() : MethodBodyStatement { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { writer.WriteLine(); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/ForStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/ForStatement.cs index f4ec5116309..647a8e2a420 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/ForStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/ForStatement.cs @@ -1,12 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections; using System.Collections.Generic; +using Microsoft.Generator.CSharp.Expressions; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Statements { - public sealed record ForStatement(AssignmentExpression? IndexerAssignment, BoolExpression? Condition, ValueExpression? IncrementExpression) : MethodBodyStatement, IEnumerable + public sealed record ForStatement(AssignmentExpression? IndexerAssignment, ValueExpression? Condition, ValueExpression? IncrementExpression) : MethodBodyStatement, IEnumerable { private readonly List _body = new(); public IReadOnlyList Body => _body; @@ -15,7 +16,7 @@ public sealed record ForStatement(AssignmentExpression? IndexerAssignment, BoolE public IEnumerator GetEnumerator() => _body.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_body).GetEnumerator(); - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { using (writer.AmbientScope()) { @@ -26,14 +27,13 @@ public override void Write(CodeWriter writer) writer.AppendRaw("; "); IncrementExpression?.Write(writer); writer.WriteRawLine(")"); - - writer.WriteRawLine("{"); - writer.WriteRawLine(""); - foreach (var bodyStatement in Body) + using (writer.Scope()) { - bodyStatement.Write(writer); + foreach (var bodyStatement in Body) + { + bodyStatement.Write(writer); + } } - writer.WriteRawLine("}"); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/ForeachStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/ForeachStatement.cs index 655b7de723e..39b0e3b2f47 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/ForeachStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/ForeachStatement.cs @@ -1,46 +1,48 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections; using System.Collections.Generic; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Snippets; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Statements { public sealed record ForeachStatement(CSharpType? ItemType, CodeWriterDeclaration Item, ValueExpression Enumerable, bool IsAsync) : MethodBodyStatement, IEnumerable { private readonly List _body = new(); public IReadOnlyList Body => _body; - public ForeachStatement(CSharpType itemType, string itemName, ValueExpression enumerable, bool isAsync, out VariableReference item) + public ForeachStatement(CSharpType itemType, string itemName, ValueExpression enumerable, bool isAsync, out VariableReferenceSnippet item) : this(itemType, new CodeWriterDeclaration(itemName), enumerable, isAsync) { - item = new VariableReference(itemType, Item); + item = new VariableReferenceSnippet(itemType, Item); } - public ForeachStatement(string itemName, EnumerableExpression enumerable, out TypedValueExpression item) + public ForeachStatement(string itemName, EnumerableSnippet enumerable, out TypedSnippet item) : this(null, new CodeWriterDeclaration(itemName), enumerable, false) { - item = new VariableReference(enumerable.ItemType, Item); + item = new VariableReferenceSnippet(enumerable.ItemType, Item); } - public ForeachStatement(string itemName, EnumerableExpression enumerable, bool isAsync, out TypedValueExpression item) + public ForeachStatement(string itemName, EnumerableSnippet enumerable, bool isAsync, out TypedSnippet item) : this(null, new CodeWriterDeclaration(itemName), enumerable, isAsync) { - item = new VariableReference(enumerable.ItemType, Item); + item = new VariableReferenceSnippet(enumerable.ItemType, Item); } - public ForeachStatement(string itemName, DictionaryExpression dictionary, out KeyValuePairExpression item) + public ForeachStatement(string itemName, DictionarySnippet dictionary, out KeyValuePairSnippet item) : this(null, new CodeWriterDeclaration(itemName), dictionary, false) { - var variable = new VariableReference(KeyValuePairExpression.GetType(dictionary.KeyType, dictionary.ValueType), Item); - item = new KeyValuePairExpression(dictionary.KeyType, dictionary.ValueType, variable); + var variable = new VariableReferenceSnippet(KeyValuePairSnippet.GetType(dictionary.KeyType, dictionary.ValueType), Item); + item = new KeyValuePairSnippet(dictionary.KeyType, dictionary.ValueType, variable); } public void Add(MethodBodyStatement statement) => _body.Add(statement); public IEnumerator GetEnumerator() => _body.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_body).GetEnumerator(); - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { using (writer.AmbientScope()) { @@ -58,13 +60,13 @@ public override void Write(CodeWriter writer) writer.Append($"{Item:D} in "); Enumerable.Write(writer); writer.WriteRawLine(")"); - - writer.WriteRawLine("{"); - foreach (var bodyStatement in Body) + using (writer.Scope()) { - bodyStatement.Write(writer); + foreach (var bodyStatement in Body) + { + bodyStatement.Write(writer); + } } - writer.WriteRawLine("}"); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/IfElsePreprocessorDirective.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/IfElsePreprocessorStatement.cs similarity index 66% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/IfElsePreprocessorDirective.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/IfElsePreprocessorStatement.cs index 3c912486b7a..3688139eca7 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/IfElsePreprocessorDirective.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/IfElsePreprocessorStatement.cs @@ -1,11 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Statements { - public sealed record IfElsePreprocessorDirective(string Condition, MethodBodyStatement If, MethodBodyStatement? Else) : MethodBodyStatement + public sealed record IfElsePreprocessorStatement(string Condition, MethodBodyStatement If, MethodBodyStatement? Else) : MethodBodyStatement { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { writer.WriteLine($"#if {Condition}"); writer.AppendRaw("\t\t\t\t"); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/IfElseStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/IfElseStatement.cs index be59c636ac8..c61dd89550e 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/IfElseStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/IfElseStatement.cs @@ -1,14 +1,17 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -namespace Microsoft.Generator.CSharp.Expressions +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Snippets; + +namespace Microsoft.Generator.CSharp.Statements { public sealed record IfElseStatement(IfStatement If, MethodBodyStatement? Else) : MethodBodyStatement { - public IfElseStatement(BoolExpression condition, MethodBodyStatement ifStatement, MethodBodyStatement? elseStatement, bool inline = false, bool addBraces = true) + public IfElseStatement(BoolSnippet condition, MethodBodyStatement ifStatement, MethodBodyStatement? elseStatement, bool inline = false, bool addBraces = true) : this(new IfStatement(condition, inline, addBraces) { ifStatement }, elseStatement) {} - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { If.Write(writer); if (Else is null) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/IfStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/IfStatement.cs index 7314187d182..dc0065abaf7 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/IfStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/IfStatement.cs @@ -1,12 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections; using System.Collections.Generic; +using Microsoft.Generator.CSharp.Expressions; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Statements { - public sealed record IfStatement(BoolExpression Condition, bool Inline = false, bool AddBraces = true) : MethodBodyStatement, IEnumerable + public sealed record IfStatement(ValueExpression Condition, bool Inline = false, bool AddBraces = true) : MethodBodyStatement, IEnumerable { private readonly List _body = new(); public MethodBodyStatement Body => _body; @@ -15,7 +16,7 @@ public sealed record IfStatement(BoolExpression Condition, bool Inline = false, public IEnumerator GetEnumerator() => _body.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_body).GetEnumerator(); - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { writer.AppendRaw("if ("); Condition.Write(writer); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/InvokeInstanceMethodStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/InvokeInstanceMethodStatement.cs index 65d21f734cd..73a61e9f66d 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/InvokeInstanceMethodStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/InvokeInstanceMethodStatement.cs @@ -1,10 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; using System.Collections.Generic; +using Microsoft.Generator.CSharp.Expressions; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Statements { public sealed record InvokeInstanceMethodStatement(ValueExpression? InstanceReference, string MethodName, IReadOnlyList Arguments, bool CallAsAsync) : MethodBodyStatement { @@ -13,7 +14,7 @@ public InvokeInstanceMethodStatement(ValueExpression? instance, string methodNam public InvokeInstanceMethodStatement(ValueExpression? instance, string methodName, ValueExpression arg1, ValueExpression arg2) : this(instance, methodName, new[] { arg1, arg2 }, false) { } public InvokeInstanceMethodStatement(ValueExpression? instance, string methodName, IReadOnlyList arguments) : this(instance, methodName, arguments, false) { } - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { new InvokeInstanceMethodExpression(InstanceReference, MethodName, Arguments, null, CallAsAsync).Write(writer); writer.WriteRawLine(";"); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/InvokeStaticMethodStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/InvokeStaticMethodStatement.cs index ec5448d09b3..aadd7e06404 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/InvokeStaticMethodStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/InvokeStaticMethodStatement.cs @@ -1,10 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; using System.Collections.Generic; +using Microsoft.Generator.CSharp.Expressions; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Statements { public sealed record InvokeStaticMethodStatement(CSharpType? MethodType, string MethodName, IReadOnlyList Arguments, IReadOnlyList? TypeArguments = null, bool CallAsExtension = false, bool CallAsAsync = false) : MethodBodyStatement { @@ -14,7 +15,7 @@ public InvokeStaticMethodStatement(CSharpType? methodType, string methodName, Va public static InvokeStaticMethodStatement Extension(CSharpType? methodType, string methodName, ValueExpression instanceReference) => new(methodType, methodName, new[] { instanceReference }, CallAsExtension: true); public static InvokeStaticMethodStatement Extension(CSharpType? methodType, string methodName, ValueExpression instanceReference, ValueExpression arg) => new(methodType, methodName, new[] { instanceReference, arg }, CallAsExtension: true); - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { new InvokeStaticMethodExpression(MethodType, MethodName, Arguments, TypeArguments, CallAsExtension, CallAsAsync).Write(writer); writer.WriteRawLine(";"); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/KeywordStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/KeywordStatement.cs index c302090b1f4..0233b5abd27 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/KeywordStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/KeywordStatement.cs @@ -1,11 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -namespace Microsoft.Generator.CSharp.Expressions +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Statements { public sealed record KeywordStatement(string Keyword, ValueExpression? Expression) : MethodBodyStatement { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { writer.AppendRaw(Keyword); if (Expression is not null) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/MethodBodyStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/MethodBodyStatement.cs index 7881a460439..8aade9e3f7c 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/MethodBodyStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/MethodBodyStatement.cs @@ -1,13 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections.Generic; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Statements { public record MethodBodyStatement { - public virtual void Write(CodeWriter writer) { } + internal virtual void Write(CodeWriter writer) { } public static implicit operator MethodBodyStatement(MethodBodyStatement[] statements) => new MethodBodyStatements(Statements: statements); public static implicit operator MethodBodyStatement(List statements) => new MethodBodyStatements(Statements: statements); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/MethodBodyStatements.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/MethodBodyStatements.cs index dfd6b7b5762..b698846f2d9 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/MethodBodyStatements.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/MethodBodyStatements.cs @@ -1,13 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections.Generic; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Statements { public record MethodBodyStatements(IReadOnlyList Statements) : MethodBodyStatement { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { foreach (var statement in Statements) { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/ParameterValidationBlock.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/ParameterValidationBlock.cs deleted file mode 100644 index 061141630a8..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/ParameterValidationBlock.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections.Generic; - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record ParameterValidationBlock(IReadOnlyList Parameters, bool IsLegacy = false) : MethodBodyStatement - { - public sealed override void Write(CodeWriter writer) - { - if (IsLegacy) - { - writer.WriteParameterNullChecks(Parameters); - } - else - { - writer.WriteParametersValidation(Parameters); - } - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/ParameterValidationStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/ParameterValidationStatement.cs new file mode 100644 index 00000000000..05c79f72afc --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/ParameterValidationStatement.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.Generator.CSharp.Providers; + +namespace Microsoft.Generator.CSharp.Statements +{ + public sealed record ParameterValidationStatement(IReadOnlyList Parameters) : MethodBodyStatement + { + internal sealed override void Write(CodeWriter writer) + { + writer.WriteParametersValidation(Parameters); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/SingleLineCommentStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/SingleLineCommentStatement.cs index bd3361e2018..5ce27857d95 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/SingleLineCommentStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/SingleLineCommentStatement.cs @@ -1,9 +1,9 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Statements { public sealed record SingleLineCommentStatement(FormattableString Message) : MethodBodyStatement { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/SwitchCase.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/SwitchCase.cs deleted file mode 100644 index cef26f34d8d..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/SwitchCase.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; - - -namespace Microsoft.Generator.CSharp.Expressions -{ - public sealed record SwitchCase(IReadOnlyList Match, MethodBodyStatement Statement, bool Inline = false, bool AddScope = false) - { - public SwitchCase(ValueExpression match, MethodBodyStatement statement, bool inline = false, bool addScope = false) : this(new[] { match }, statement, inline, addScope) { } - - public static SwitchCase Default(MethodBodyStatement statement, bool inline = false, bool addScope = false) => new(Array.Empty(), statement, inline, addScope); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/SwitchCaseStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/SwitchCaseStatement.cs new file mode 100644 index 00000000000..fa6d80fa44e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/SwitchCaseStatement.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Statements +{ + public sealed record SwitchCaseStatement(IReadOnlyList Matches, MethodBodyStatement Statement, bool Inline = false, bool AddScope = false) : MethodBodyStatement + { + public SwitchCaseStatement(ValueExpression match, MethodBodyStatement statement, bool inline = false, bool addScope = false) : this(new[] { match }, statement, inline, addScope) { } + + public static SwitchCaseStatement Default(MethodBodyStatement statement, bool inline = false, bool addScope = false) => new(Array.Empty(), statement, inline, addScope); + + internal override void Write(CodeWriter writer) + { + if (Matches.Any()) + { + for (var i = 0; i < Matches.Count; i++) + { + ValueExpression? match = Matches[i]; + writer.AppendRaw("case "); + match.Write(writer); + if (i < Matches.Count - 1) + { + writer.WriteRawLine(":"); + } + } + } + else + { + writer.AppendRaw("default"); + } + + writer.AppendRaw(": "); + if (!Inline) + { + writer.WriteLine(); + } + + if (AddScope) + { + using (writer.Scope()) + { + Statement.Write(writer); + } + } + else + { + Statement.Write(writer); + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/SwitchStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/SwitchStatement.cs index 2bc448a1157..c206ad0a10e 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/SwitchStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/SwitchStatement.cs @@ -1,27 +1,27 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections; using System.Collections.Generic; -using System.Linq; +using Microsoft.Generator.CSharp.Expressions; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Statements { - public sealed record SwitchStatement(ValueExpression MatchExpression) : MethodBodyStatement, IEnumerable + public sealed record SwitchStatement(ValueExpression MatchExpression) : MethodBodyStatement, IEnumerable { - public SwitchStatement(ValueExpression matchExpression, IEnumerable cases) : this(matchExpression) + public SwitchStatement(ValueExpression matchExpression, IEnumerable cases) : this(matchExpression) { _cases.AddRange(cases); } - private readonly List _cases = new(); - public IReadOnlyList Cases => _cases; + private readonly List _cases = new(); + public IReadOnlyList Cases => _cases; - public void Add(SwitchCase statement) => _cases.Add(statement); - public IEnumerator GetEnumerator() => _cases.GetEnumerator(); + public void Add(SwitchCaseStatement statement) => _cases.Add(statement); + public IEnumerator GetEnumerator() => _cases.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_cases).GetEnumerator(); - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { using (writer.AmbientScope()) { @@ -31,41 +31,7 @@ public override void Write(CodeWriter writer) writer.WriteRawLine("{"); foreach (var switchCase in Cases) { - if (switchCase.Match.Any()) - { - for (var i = 0; i < switchCase.Match.Count; i++) - { - ValueExpression? match = switchCase.Match[i]; - writer.AppendRaw("case "); - match.Write(writer); - if (i < switchCase.Match.Count - 1) - { - writer.WriteRawLine(":"); - } - } - } - else - { - writer.AppendRaw("default"); - } - - writer.AppendRaw(": "); - if (!switchCase.Inline) - { - writer.WriteLine(); - } - - if (switchCase.AddScope) - { - using (writer.Scope()) - { - switchCase.Statement.Write(writer); - } - } - else - { - switchCase.Statement.Write(writer); - } + switchCase.Write(writer); } writer.WriteRawLine("}"); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/ThrowStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/ThrowStatement.cs index f334ecd5809..35ed3f06ffb 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/ThrowStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/ThrowStatement.cs @@ -1,11 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -namespace Microsoft.Generator.CSharp.Expressions +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Statements { public sealed record ThrowStatement(ValueExpression ThrowExpression) : MethodBodyStatement { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { ThrowExpression.Write(writer); writer.WriteRawLine(";"); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/TryCatchFinallyStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/TryCatchFinallyStatement.cs index 09fcd86a0ea..d8c1261192d 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/TryCatchFinallyStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/TryCatchFinallyStatement.cs @@ -1,22 +1,23 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; using System.Collections.Generic; +using Microsoft.Generator.CSharp.Expressions; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Statements { - public sealed record TryCatchFinallyStatement(MethodBodyStatement Try, IReadOnlyList Catches, MethodBodyStatement? Finally) : MethodBodyStatement + public sealed record TryCatchFinallyStatement(MethodBodyStatement Try, IReadOnlyList Catches, MethodBodyStatement? Finally) : MethodBodyStatement { - public TryCatchFinallyStatement(MethodBodyStatement Try) : this(Try, Array.Empty(), null) + public TryCatchFinallyStatement(MethodBodyStatement Try) : this(Try, Array.Empty(), null) { } - public TryCatchFinallyStatement(MethodBodyStatement Try, CatchStatement Catch, MethodBodyStatement? Finally = null) : this(Try, [Catch], Finally) + public TryCatchFinallyStatement(MethodBodyStatement Try, CatchExpression Catch, MethodBodyStatement? Finally = null) : this(Try, [Catch], Finally) { } - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { writer.WriteRawLine("try"); writer.WriteRawLine("{"); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/UnaryOperatorStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/UnaryOperatorStatement.cs index 11dde09ebf7..2f039b2c28c 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/UnaryOperatorStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/UnaryOperatorStatement.cs @@ -1,11 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -namespace Microsoft.Generator.CSharp.Expressions +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Statements { public sealed record UnaryOperatorStatement(UnaryOperatorExpression Expression) : MethodBodyStatement { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { Expression.Write(writer); writer.WriteRawLine(";"); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/UsingDeclareVariableStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/UsingDeclareVariableStatement.cs index 58d5a597474..8fc6987a9c7 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/UsingDeclareVariableStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/UsingDeclareVariableStatement.cs @@ -1,11 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -namespace Microsoft.Generator.CSharp.Expressions +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Statements { public sealed record UsingDeclareVariableStatement(CSharpType? Type, CodeWriterDeclaration Name, ValueExpression Value) : MethodBodyStatement { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { if (Type != null) { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/UsingScopeStatement.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/UsingScopeStatement.cs index f3b0d90c3da..d345d5f6030 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/UsingScopeStatement.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Statements/UsingScopeStatement.cs @@ -1,26 +1,28 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections; using System.Collections.Generic; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Snippets; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Statements { public sealed record UsingScopeStatement(CSharpType? Type, CodeWriterDeclaration Variable, ValueExpression Value) : MethodBodyStatement, IEnumerable { private readonly List _body = new(); public IReadOnlyList Body => _body; - public UsingScopeStatement(CSharpType type, string variableName, ValueExpression value, out VariableReference variable) : this(type, new CodeWriterDeclaration(variableName), value) + public UsingScopeStatement(CSharpType type, string variableName, ValueExpression value, out VariableReferenceSnippet variable) : this(type, new CodeWriterDeclaration(variableName), value) { - variable = new VariableReference(type, Variable); + variable = new VariableReferenceSnippet(type, Variable); } public void Add(MethodBodyStatement statement) => _body.Add(statement); public IEnumerator GetEnumerator() => _body.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_body).GetEnumerator(); - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { using (writer.AmbientScope()) { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/BinaryDataSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/BinaryDataSnippet.cs new file mode 100644 index 00000000000..875c00b7e2a --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/BinaryDataSnippet.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record BinaryDataSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) + { + private static readonly CSharpType _binaryDataType = typeof(BinaryData); + + public FrameworkTypeSnippet ToObjectFromJson(Type responseType) + => new(responseType, new InvokeInstanceMethodExpression(Untyped, nameof(BinaryData.ToObjectFromJson), Array.Empty(), new[] { new CSharpType(responseType) }, false)); + + public static BinaryDataSnippet FromStream(StreamSnippet stream, bool async) + { + var methodName = async ? nameof(BinaryData.FromStreamAsync) : nameof(BinaryData.FromStream); + return new BinaryDataSnippet(new InvokeStaticMethodExpression(_binaryDataType, methodName, stream, callAsAsync: async)); + } + + public static BinaryDataSnippet FromStream(ValueExpression stream, bool async) + { + var methodName = async ? nameof(BinaryData.FromStreamAsync) : nameof(BinaryData.FromStream); + return new(new InvokeStaticMethodExpression(_binaryDataType, methodName, stream, callAsAsync: async)); + } + + public ValueExpression ToMemory() => Untyped.Invoke(nameof(BinaryData.ToMemory)); + + public StreamSnippet ToStream() => new(Untyped.Invoke(nameof(BinaryData.ToStream))); + + public ListSnippet ToArray() => new(typeof(byte[]), Untyped.Invoke(nameof(BinaryData.ToArray))); + + public static BinaryDataSnippet FromBytes(ValueExpression data) + => new(new InvokeStaticMethodExpression(_binaryDataType, nameof(BinaryData.FromBytes), data)); + + public static BinaryDataSnippet FromObjectAsJson(ValueExpression data) + => new(new InvokeStaticMethodExpression(_binaryDataType, nameof(BinaryData.FromObjectAsJson), data)); + + public static BinaryDataSnippet FromString(ValueExpression data) + => new(new InvokeStaticMethodExpression(_binaryDataType, nameof(BinaryData.FromString), data)); + + //public static implicit operator ValueExpression(BinaryDataExpression binaryData) + // => binaryData.Untyped; + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/BoolSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/BoolSnippet.cs new file mode 100644 index 00000000000..8c49f79be2a --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/BoolSnippet.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record BoolSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) + { + public BoolSnippet Or(ValueExpression other) => new(new BinaryOperatorExpression("||", this, other)); + + public BoolSnippet And(ValueExpression other) => new(new BinaryOperatorExpression("&&", this, other)); + + public static BoolSnippet True { get; } = Snippet.True; + + public static BoolSnippet False { get; } = Snippet.False; + + public static BoolSnippet Is(ValueExpression untyped, CSharpType comparisonType) => new(new BinaryOperatorExpression("is", untyped, comparisonType)); + + public static BoolSnippet Is(ValueExpression untyped, DeclarationExpression declaration) => new(new BinaryOperatorExpression("is", untyped, declaration)); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/CancellationTokenSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/CancellationTokenSnippet.cs new file mode 100644 index 00000000000..69e7dd73188 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/CancellationTokenSnippet.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading; +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record CancellationTokenSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) + { + public BoolSnippet CanBeCanceled => new(Property(nameof(CancellationToken.CanBeCanceled))); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/CharSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/CharSnippet.cs new file mode 100644 index 00000000000..ed02ced9a8f --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/CharSnippet.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record CharSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) + { + public StringSnippet InvokeToString(ValueExpression cultureInfo) => new(Untyped.Invoke(nameof(char.ToString), cultureInfo)); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/DateTimeOffsetSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/DateTimeOffsetSnippet.cs new file mode 100644 index 00000000000..7ef4ab85fde --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/DateTimeOffsetSnippet.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record DateTimeOffsetSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) + { + public static DateTimeOffsetSnippet Now => new(StaticProperty(nameof(DateTimeOffset.Now))); + public static DateTimeOffsetSnippet UtcNow => new(StaticProperty(nameof(DateTimeOffset.UtcNow))); + + public static DateTimeOffsetSnippet FromUnixTimeSeconds(ValueExpression expression) + => new(InvokeStatic(nameof(DateTimeOffset.FromUnixTimeSeconds), expression)); + + public StringSnippet InvokeToString(StringSnippet format, ValueExpression formatProvider) + => new(Untyped.Invoke(nameof(DateTimeOffset.ToString), new[] { format, formatProvider })); + + public LongSnippet ToUnixTimeSeconds() + => new(Untyped.Invoke(nameof(DateTimeOffset.ToUnixTimeSeconds))); + + public DateTimeOffsetSnippet ToUniversalTime() + => new(Untyped.Invoke(nameof(DateTimeOffset.ToUniversalTime))); + + public static DateTimeOffsetSnippet Parse(string s) => Parse(Snippet.Literal(s)); + + public static DateTimeOffsetSnippet Parse(ValueExpression value) + => new(InvokeStatic(nameof(DateTimeOffset.Parse), value)); + + public static DateTimeOffsetSnippet Parse(ValueExpression value, ValueExpression formatProvider, ValueExpression style) + => new(InvokeStatic(nameof(DateTimeOffset.Parse), new[] { value, formatProvider, style })); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/DictionarySnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/DictionarySnippet.cs new file mode 100644 index 00000000000..e7908866bf5 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/DictionarySnippet.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Statements; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record DictionarySnippet(CSharpType KeyType, CSharpType ValueType, ValueExpression Untyped) : TypedSnippet(new CSharpType(typeof(Dictionary<,>), false, KeyType, ValueType), Untyped) + { + public MethodBodyStatement Add(ValueExpression key, ValueExpression value) + => new InvokeInstanceMethodStatement(Untyped, nameof(Dictionary.Add), key, value); + + public MethodBodyStatement Add(KeyValuePairSnippet pair) + => new InvokeInstanceMethodStatement(Untyped, nameof(Dictionary.Add), pair); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/DoubleSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/DoubleSnippet.cs new file mode 100644 index 00000000000..9dcffeed5cf --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/DoubleSnippet.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record DoubleSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) + { + public static DoubleSnippet MaxValue => new(StaticProperty(nameof(double.MaxValue))); + + public static BoolSnippet IsNan(ValueExpression d) => new(new InvokeStaticMethodExpression(typeof(double), nameof(double.IsNaN), new[] { d })); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/EnumerableSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/EnumerableSnippet.cs new file mode 100644 index 00000000000..0d465678dfe --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/EnumerableSnippet.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record EnumerableSnippet(CSharpType ItemType, ValueExpression Untyped) : TypedSnippet(new CSharpType(typeof(IEnumerable<>), false, ItemType), Untyped) + { + public BoolSnippet Any() => new(new InvokeStaticMethodExpression(typeof(Enumerable), nameof(Enumerable.Any), new[] { Untyped }, CallAsExtension: true)); + public EnumerableSnippet Select(TypedSnippet selector) => new(selector.Type, new InvokeStaticMethodExpression(typeof(Enumerable), nameof(Enumerable.Select), new[] { Untyped, selector }, CallAsExtension: true)); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/EnvironmentSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/EnvironmentSnippet.cs new file mode 100644 index 00000000000..6ca06d38e48 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/EnvironmentSnippet.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record EnvironmentSnippet(ValueExpression Untyped) : TypedSnippet(typeof(Environment), Untyped) + { + public static StringSnippet NewLine() => new(new TypeReferenceExpression(typeof(Environment)).Property(nameof(Environment.NewLine))); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/FrameworkTypeSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/FrameworkTypeSnippet.cs new file mode 100644 index 00000000000..4fa3f7c240c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/FrameworkTypeSnippet.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + /// + /// Represents expression which has a return value of a framework type. + /// + public sealed record FrameworkTypeSnippet(Type FrameworkType, ValueExpression Untyped) : TypedSnippet(FrameworkType, Untyped); +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/IntSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/IntSnippet.cs new file mode 100644 index 00000000000..3528ea0e389 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/IntSnippet.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record IntSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) + { + public static IntSnippet MaxValue => new(StaticProperty(nameof(int.MaxValue))); + public IntSnippet Add(IntSnippet value) => Operator("+", value); + public IntSnippet Minus(IntSnippet value) => Operator("-", value); + public IntSnippet Multiply(IntSnippet value) => Operator("*", value); + public IntSnippet DivideBy(IntSnippet value) => Operator("/", value); + + public static IntSnippet operator +(IntSnippet left, IntSnippet right) => left.Add(right); + public static IntSnippet operator -(IntSnippet left, IntSnippet right) => left.Minus(right); + public static IntSnippet operator *(IntSnippet left, IntSnippet right) => left.Multiply(right); + public static IntSnippet operator /(IntSnippet left, IntSnippet right) => left.DivideBy(right); + + private IntSnippet Operator(string op, IntSnippet other) => new(new BinaryOperatorExpression(op, this, other)); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/JsonDocumentSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/JsonDocumentSnippet.cs new file mode 100644 index 00000000000..0dc3de32a82 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/JsonDocumentSnippet.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text.Json; +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record JsonDocumentSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) + { + public JsonElementSnippet RootElement => new(Property(nameof(JsonDocument.RootElement))); + + public static JsonDocumentSnippet ParseValue(ValueExpression reader) => new(InvokeStatic(nameof(JsonDocument.ParseValue), reader)); + public static JsonDocumentSnippet Parse(ValueExpression json) => new(InvokeStatic(nameof(JsonDocument.Parse), json)); + public static JsonDocumentSnippet Parse(BinaryDataSnippet binaryData) => new(InvokeStatic(nameof(JsonDocument.Parse), binaryData)); + public static JsonDocumentSnippet Parse(StreamSnippet stream) => new(InvokeStatic(nameof(JsonDocument.Parse), stream)); + + public static JsonDocumentSnippet Parse(StreamSnippet stream, bool async) + { + // Sync and async methods have different set of parameters + return async + ? new JsonDocumentSnippet(InvokeStatic(nameof(JsonDocument.ParseAsync), new[] { stream, Snippet.Default, Snippet.Default }, true)) + : new JsonDocumentSnippet(InvokeStatic(nameof(JsonDocument.Parse), stream)); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/JsonElementSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/JsonElementSnippet.cs new file mode 100644 index 00000000000..d551b54f17c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/JsonElementSnippet.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text.Json; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Statements; +using static Microsoft.Generator.CSharp.Snippets.Snippet; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record JsonElementSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) + { + public JsonValueKindSnippet ValueKind => new(Property(nameof(JsonElement.ValueKind))); + public EnumerableSnippet EnumerateArray() => new(typeof(JsonElement), Untyped.Invoke(nameof(JsonElement.EnumerateArray))); + public EnumerableSnippet EnumerateObject() => new(typeof(JsonProperty), Untyped.Invoke(nameof(JsonElement.EnumerateObject))); + public JsonElementSnippet this[int index] => new(new IndexerExpression(Untyped, Int(index))); + public JsonElementSnippet GetProperty(string propertyName) => new(Untyped.Invoke(nameof(JsonElement.GetProperty), Literal(propertyName))); + + public ValueExpression InvokeClone() => Untyped.Invoke(nameof(JsonElement.Clone)); + public ValueExpression GetArrayLength() => Untyped.Invoke(nameof(JsonElement.GetArrayLength)); + public ValueExpression GetBoolean() => Untyped.Invoke(nameof(JsonElement.GetBoolean)); + public ValueExpression GetBytesFromBase64() => Untyped.Invoke(nameof(JsonElement.GetBytesFromBase64)); + public ValueExpression GetDateTime() => Untyped.Invoke(nameof(JsonElement.GetDateTime)); + public ValueExpression GetDecimal() => Untyped.Invoke(nameof(JsonElement.GetDecimal)); + public ValueExpression GetDouble() => Untyped.Invoke(nameof(JsonElement.GetDouble)); + public ValueExpression GetGuid() => Untyped.Invoke(nameof(JsonElement.GetGuid)); + public ValueExpression GetSByte() => Untyped.Invoke(nameof(JsonElement.GetSByte)); + public ValueExpression GetByte() => Untyped.Invoke(nameof(JsonElement.GetByte)); + public ValueExpression GetInt16() => Untyped.Invoke(nameof(JsonElement.GetInt16)); + public ValueExpression GetInt32() => Untyped.Invoke(nameof(JsonElement.GetInt32)); + public ValueExpression GetInt64() => Untyped.Invoke(nameof(JsonElement.GetInt64)); + public StringSnippet GetRawText() => new(Untyped.Invoke(nameof(JsonElement.GetRawText))); + public ValueExpression GetSingle() => Untyped.Invoke(nameof(JsonElement.GetSingle)); + public StringSnippet GetString() => new(Untyped.Invoke(nameof(JsonElement.GetString))); + + public BoolSnippet ValueKindEqualsNull() + => new(new BinaryOperatorExpression("==", Property(nameof(JsonElement.ValueKind)), FrameworkEnumValue(JsonValueKind.Null))); + + public BoolSnippet ValueKindNotEqualsUndefined() + => new(new BinaryOperatorExpression("!=", Property(nameof(JsonElement.ValueKind)), FrameworkEnumValue(JsonValueKind.Undefined))); + + public BoolSnippet ValueKindEqualsString() + => new(new BinaryOperatorExpression("==", Property(nameof(JsonElement.ValueKind)), FrameworkEnumValue(JsonValueKind.String))); + + public MethodBodyStatement WriteTo(ValueExpression writer) => new InvokeInstanceMethodStatement(Untyped, nameof(JsonElement.WriteTo), new[] { writer }, false); + + public BoolSnippet TryGetProperty(string propertyName, out JsonElementSnippet discriminator) + { + var discriminatorDeclaration = new VariableReferenceSnippet(typeof(JsonElement), "discriminator"); + discriminator = new JsonElementSnippet(discriminatorDeclaration); + var invocation = new InvokeInstanceMethodExpression(this, nameof(JsonElement.TryGetProperty), [Literal(propertyName), new DeclarationExpression(discriminatorDeclaration.Type, discriminatorDeclaration.Declaration, true)], null, false); + return new BoolSnippet(invocation); + } + + public BoolSnippet TryGetInt32(out IntSnippet intValue) + { + var intValueDeclaration = new VariableReferenceSnippet(typeof(int), "intValue"); + intValue = new IntSnippet(intValueDeclaration); + var invocation = new InvokeInstanceMethodExpression(this, nameof(JsonElement.TryGetInt32), [new DeclarationExpression(intValueDeclaration.Type, intValueDeclaration.Declaration, true)], null, false); + return new BoolSnippet(invocation); + } + + public BoolSnippet TryGetInt64(out LongSnippet longValue) + { + var longValueDeclaration = new VariableReferenceSnippet(typeof(long), "longValue"); + longValue = new LongSnippet(longValueDeclaration); + var invocation = new InvokeInstanceMethodExpression(this, nameof(JsonElement.TryGetInt64), [new DeclarationExpression(longValueDeclaration.Type, longValueDeclaration.Declaration, true)], null, false); + return new BoolSnippet(invocation); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/JsonPropertySnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/JsonPropertySnippet.cs new file mode 100644 index 00000000000..c1f5fc82ddf --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/JsonPropertySnippet.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text.Json; +using Microsoft.Generator.CSharp.Expressions; +using static Microsoft.Generator.CSharp.Snippets.Snippet; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record JsonPropertySnippet(ValueExpression Untyped) : TypedSnippet(Untyped) + { + public StringSnippet Name => new(Property(nameof(JsonProperty.Name))); + public JsonElementSnippet Value => new(Property(nameof(JsonProperty.Value))); + + public BoolSnippet NameEquals(string value) => new(Untyped.Invoke(nameof(JsonProperty.NameEquals), LiteralU8(value))); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/JsonValueKindSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/JsonValueKindSnippet.cs new file mode 100644 index 00000000000..3884eea9dbe --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/JsonValueKindSnippet.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text.Json; +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record JsonValueKindSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) + { + public static JsonValueKindSnippet String => InvokeStaticProperty(nameof(JsonValueKind.String)); + public static JsonValueKindSnippet Number => InvokeStaticProperty(nameof(JsonValueKind.Number)); + public static JsonValueKindSnippet True => InvokeStaticProperty(nameof(JsonValueKind.True)); + public static JsonValueKindSnippet False => InvokeStaticProperty(nameof(JsonValueKind.False)); + public static JsonValueKindSnippet Undefined => InvokeStaticProperty(nameof(JsonValueKind.Undefined)); + public static JsonValueKindSnippet Null => InvokeStaticProperty(nameof(JsonValueKind.Null)); + public static JsonValueKindSnippet Array => InvokeStaticProperty(nameof(JsonValueKind.Array)); + public static JsonValueKindSnippet Object => InvokeStaticProperty(nameof(JsonValueKind.Object)); + + private static JsonValueKindSnippet InvokeStaticProperty(string name) + => new(new MemberExpression(typeof(JsonValueKind), name)); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/KeyValuePairSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/KeyValuePairSnippet.cs new file mode 100644 index 00000000000..3f1a5347fd9 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/KeyValuePairSnippet.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record KeyValuePairSnippet(CSharpType KeyType, CSharpType ValueType, ValueExpression Untyped) : TypedSnippet(GetType(KeyType, ValueType), Untyped) + { + public ValueExpression Key => new MemberExpression(Untyped, nameof(KeyValuePair.Key)); + public ValueExpression Value => new MemberExpression(Untyped, nameof(KeyValuePair.Value)); + + public static CSharpType GetType(CSharpType keyType, CSharpType valueType) => new(typeof(KeyValuePair<,>), false, keyType, valueType); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/ListSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/ListSnippet.cs new file mode 100644 index 00000000000..62407e318d4 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/ListSnippet.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Statements; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record ListSnippet(CSharpType ItemType, ValueExpression Untyped) : TypedSnippet(new CSharpType(typeof(List<>), ItemType), Untyped) + { + public MethodBodyStatement Add(ValueExpression item) => new InvokeInstanceMethodStatement(Untyped, nameof(List.Add), item); + + public ValueExpression ToArray() => Untyped.Invoke(nameof(List.ToArray)); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/LongSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/LongSnippet.cs new file mode 100644 index 00000000000..4f9286f3708 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/LongSnippet.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record LongSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) + { + public StringSnippet InvokeToString(ValueExpression formatProvider) + => new(Untyped.Invoke(nameof(long.ToString), formatProvider)); + + public static LongSnippet Parse(StringSnippet value, ValueExpression formatProvider) + => new(new InvokeStaticMethodExpression(typeof(long), nameof(long.Parse), new[] { value, formatProvider })); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/ParameterReferenceSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/ParameterReferenceSnippet.cs new file mode 100644 index 00000000000..6a2f5747fcb --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/ParameterReferenceSnippet.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Providers; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record ParameterReferenceSnippet(ParameterProvider Parameter) : TypedSnippet(Parameter.Type, new UntypedParameterReference(Parameter)) + { + private record UntypedParameterReference(ParameterProvider Parameter) : ValueExpression + { + internal override void Write(CodeWriter writer) + { + writer.AppendRawIf("ref ", Parameter.IsRef); + writer.Append($"{Parameter.Name:I}"); + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/StreamReaderSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/StreamReaderSnippet.cs new file mode 100644 index 00000000000..f308abc7cf3 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/StreamReaderSnippet.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record StreamReaderSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) + { + public StringSnippet ReadToEnd(bool async) + { + var methodName = async ? nameof(StreamReader.ReadToEndAsync) : nameof(StreamReader.ReadToEnd); + return new(Untyped.Invoke(methodName, Array.Empty(), async)); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/StreamSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/StreamSnippet.cs new file mode 100644 index 00000000000..e250ca21470 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/StreamSnippet.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Statements; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record StreamSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) + { + public MethodBodyStatement CopyTo(StreamSnippet destination) => new InvokeInstanceMethodStatement(Untyped, nameof(Stream.CopyTo), destination); + + public ValueExpression Position => new MemberExpression(this, nameof(Stream.Position)); + public ValueExpression GetBuffer => new InvokeInstanceMethodExpression(this, nameof(MemoryStream.GetBuffer), Array.Empty(), null, false); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/StringBuilderExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/StringBuilderSnippet.cs similarity index 61% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/StringBuilderExpression.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/StringBuilderSnippet.cs index f4b0774f48f..bae8d7fb69e 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/StringBuilderExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/StringBuilderSnippet.cs @@ -2,24 +2,26 @@ // Licensed under the MIT License. using System.Text; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Statements; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Snippets { - public sealed record StringBuilderExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + public sealed record StringBuilderSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) { - public IntExpression Length => new(Property(nameof(StringBuilder.Length))); + public IntSnippet Length => new(Property(nameof(StringBuilder.Length))); - public MethodBodyStatement Append(StringExpression value) => new InvokeInstanceMethodStatement(Untyped, nameof(StringBuilder.Append), value); + public MethodBodyStatement Append(StringSnippet value) => new InvokeInstanceMethodStatement(Untyped, nameof(StringBuilder.Append), value); - public MethodBodyStatement AppendLine(StringExpression value) => new InvokeInstanceMethodStatement(Untyped, nameof(StringBuilder.AppendLine), value); + public MethodBodyStatement AppendLine(StringSnippet value) => new InvokeInstanceMethodStatement(Untyped, nameof(StringBuilder.AppendLine), value); public MethodBodyStatement Append(ValueExpression value) => new InvokeInstanceMethodStatement(Untyped, nameof(StringBuilder.Append), value); public MethodBodyStatement AppendLine(ValueExpression value) => new InvokeInstanceMethodStatement(Untyped, nameof(StringBuilder.AppendLine), value); - public MethodBodyStatement Append(string value) => Append(Snippets.Literal(value)); + public MethodBodyStatement Append(string value) => Append(Snippet.Literal(value)); - public MethodBodyStatement AppendLine(string value) => AppendLine(Snippets.Literal(value)); + public MethodBodyStatement AppendLine(string value) => AppendLine(Snippet.Literal(value)); public MethodBodyStatement Append(FormattableStringExpression value) => new InvokeInstanceMethodStatement(Untyped, nameof(StringBuilder.Append), value); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/StringExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/StringSnippet.cs similarity index 51% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/StringExpression.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/StringSnippet.cs index 53359e378db..39028b2c0af 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/StringExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/StringSnippet.cs @@ -3,28 +3,29 @@ using System; using System.Linq; +using Microsoft.Generator.CSharp.Expressions; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Snippets { - public sealed record StringExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + public sealed record StringSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) { - public CharExpression Index(ValueExpression index) => new(new IndexerExpression(this, index)); - public CharExpression Index(int index) => Index(Snippets.Literal(index)); + public CharSnippet Index(ValueExpression index) => new(new IndexerExpression(this, index)); + public CharSnippet Index(int index) => Index(Snippet.Literal(index)); public ValueExpression Length => Property(nameof(string.Length)); - public static BoolExpression Equals(StringExpression left, StringExpression right, StringComparison comparisonType) - => new(InvokeStatic(nameof(string.Equals), new[] { left, right, Snippets.FrameworkEnumValue(comparisonType) })); + public static BoolSnippet Equals(StringSnippet left, StringSnippet right, StringComparison comparisonType) + => new(InvokeStatic(nameof(string.Equals), new[] { left, right, Snippet.FrameworkEnumValue(comparisonType) })); - public static StringExpression Format(StringExpression format, params ValueExpression[] args) + public static StringSnippet Format(StringSnippet format, params ValueExpression[] args) => new(new InvokeStaticMethodExpression(typeof(string), nameof(string.Format), args.Prepend(format).ToArray())); - public static BoolExpression IsNullOrWhiteSpace(StringExpression value, params ValueExpression[] args) + public static BoolSnippet IsNullOrWhiteSpace(StringSnippet value, params ValueExpression[] args) => new(new InvokeStaticMethodExpression(typeof(string), nameof(string.IsNullOrWhiteSpace), args.Prepend(value).ToArray())); - public static StringExpression Join(ValueExpression separator, ValueExpression values) + public static StringSnippet Join(ValueExpression separator, ValueExpression values) => new(new InvokeStaticMethodExpression(typeof(string), nameof(string.Join), new[] { separator, values })); - public StringExpression Substring(ValueExpression startIndex) + public StringSnippet Substring(ValueExpression startIndex) => new(new InvokeInstanceMethodExpression(this, nameof(string.Substring), new[] { startIndex }, null, false)); public ValueExpression ToCharArray() => new InvokeInstanceMethodExpression(this, nameof(string.ToCharArray), Array.Empty(), null, false); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/TimeSpanSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/TimeSpanSnippet.cs new file mode 100644 index 00000000000..8f38db75e3d --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/TimeSpanSnippet.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record TimeSpanSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) + { + public StringSnippet InvokeToString(string? format) => new(Untyped.Invoke(nameof(TimeSpan.ToString), [Snippet.Literal(format)])); + public StringSnippet InvokeToString(ValueExpression format, ValueExpression formatProvider) + => new(Untyped.Invoke(nameof(TimeSpan.ToString), new[] { format, formatProvider })); + + public static TimeSpanSnippet FromSeconds(ValueExpression value) => new(InvokeStatic(nameof(TimeSpan.FromSeconds), value)); + + public static TimeSpanSnippet ParseExact(ValueExpression value, ValueExpression format, ValueExpression formatProvider) + => new(new InvokeStaticMethodExpression(typeof(TimeSpan), nameof(TimeSpan.ParseExact), new[] { value, format, formatProvider })); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/TypeProviderSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/TypeProviderSnippet.cs new file mode 100644 index 00000000000..2fcf407eb28 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/TypeProviderSnippet.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Providers; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public record TypeProviderSnippet(TypeProvider TypeProvider, ValueExpression Untyped) : TypedSnippet(TypeProvider.Type, Untyped) + { + public static MemberExpression FromResponseDelegate(TypeProvider typeProvider) + => new(new TypeReferenceExpression(typeProvider.Type), CodeModelPlugin.Instance.Configuration.ApiTypes.FromResponseName); + + public static MemberExpression DeserializeDelegate(TypeProvider typeProvider) + => new(new TypeReferenceExpression(typeProvider.Type), $"Deserialize{typeProvider.Name}"); + + public static TypeProviderSnippet Deserialize(TypeProvider typeProvider, ValueExpression element, ValueExpression? options = null) + { + var arguments = options == null ? new[] { element } : new[] { element, options }; + return new(typeProvider, new InvokeStaticMethodExpression(typeProvider.Type, $"Deserialize{typeProvider.Name}", arguments)); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/TypedSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/TypedSnippet.cs new file mode 100644 index 00000000000..b6c34d2cd5d --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/TypedSnippet.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + /// + /// A wrapper around ValueExpression that also specifies return type of the expression + /// Return type doesn't affect how expression is written, but helps creating strong-typed helpers to create value expressions. + /// + /// Type expected to be returned by value expression. + /// + public abstract record TypedSnippet(CSharpType Type, ValueExpression Untyped) + { + public virtual ValueExpression Property(string propertyName, bool nullConditional = false) + => new MemberExpression(nullConditional ? new NullConditionalExpression(this) : this, propertyName); + + public ValueExpression NullableStructValue() => Type is { IsNullable: true, IsValueType: true } ? new MemberExpression(this, nameof(Nullable.Value)) : this; + + public ValueExpression NullConditional() => Type.IsNullable ? new NullConditionalExpression(this) : this; + + protected static ValueExpression ValidateType(TypedSnippet typed, CSharpType type) + { + if (type.Equals(typed.Type, ignoreNullable: true)) + { + return typed; + } + + throw new InvalidOperationException($"Expression with return type {typed.Type.Name} is cast to type {type.Name}"); + } + + public static implicit operator ValueExpression(TypedSnippet typed) => typed.Untyped; + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypedValueExpressionOfT.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/TypedSnippetOfT.cs similarity index 75% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypedValueExpressionOfT.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/TypedSnippetOfT.cs index 6522340608a..41b12fdeccd 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/TypedValueExpressionOfT.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/TypedSnippetOfT.cs @@ -1,14 +1,15 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Generator.CSharp.Expressions; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Snippets { #pragma warning disable SA1649 // File name should match first type name - public abstract record TypedValueExpression(ValueExpression Untyped) : TypedValueExpression(typeof(T), ValidateType(Untyped, typeof(T))) + public abstract record TypedSnippet(ValueExpression Untyped) : TypedSnippet(typeof(T), ValidateType(Untyped, typeof(T))) #pragma warning restore SA1649 // File name should match first type name { protected static MemberExpression StaticProperty(string name) => new(typeof(T), name); @@ -54,31 +55,7 @@ protected InvokeStaticMethodExpression InvokeExtension(CSharpType extensionType, private static ValueExpression ValidateType(ValueExpression untyped, Type type) { -#if DEBUG - if (untyped is not TypedValueExpression typed) - { - return untyped; - } - - if (typed.Type.IsFrameworkType) - { - if (typed.Type.FrameworkType.IsGenericTypeDefinition && type.IsGenericType) - { - if (typed.Type.FrameworkType.MakeGenericType(type.GetGenericArguments()).IsAssignableTo(type)) - { - return typed.Untyped; - } - } - else if (typed.Type.FrameworkType.IsAssignableTo(type)) - { - return typed.Untyped; - } - } - - throw new InvalidOperationException($"Expression with return type {typed.Type.Name} is cast to type {type.Name}"); -#else return untyped; -#endif } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/Utf8JsonWriterExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/Utf8JsonWriterSnippet.cs similarity index 79% rename from packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/Utf8JsonWriterExpression.cs rename to packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/Utf8JsonWriterSnippet.cs index 9471439d761..f2cf5c0af4b 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Expressions/Utf8JsonWriterExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/Utf8JsonWriterSnippet.cs @@ -3,14 +3,16 @@ using System; using System.Text.Json; -using static Microsoft.Generator.CSharp.Expressions.Snippets; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Statements; +using static Microsoft.Generator.CSharp.Snippets.Snippet; -namespace Microsoft.Generator.CSharp.Expressions +namespace Microsoft.Generator.CSharp.Snippets { - public sealed record Utf8JsonWriterExpression(ValueExpression Untyped) : TypedValueExpression(Untyped) + public sealed record Utf8JsonWriterSnippet(ValueExpression Untyped) : TypedSnippet(Untyped) { - public LongExpression BytesCommitted => new(Property(nameof(Utf8JsonWriter.BytesCommitted))); - public LongExpression BytesPending => new(Property(nameof(Utf8JsonWriter.BytesPending))); + public LongSnippet BytesCommitted => new(Property(nameof(Utf8JsonWriter.BytesCommitted))); + public LongSnippet BytesPending => new(Property(nameof(Utf8JsonWriter.BytesPending))); public MethodBodyStatement WriteStartObject() => new InvokeInstanceMethodStatement(Untyped, nameof(Utf8JsonWriter.WriteStartObject)); public MethodBodyStatement WriteEndObject() => new InvokeInstanceMethodStatement(Untyped, nameof(Utf8JsonWriter.WriteEndObject)); @@ -36,16 +38,16 @@ public MethodBodyStatement WriteRawValue(ValueExpression value) => new InvokeInstanceMethodStatement(Untyped, nameof(Utf8JsonWriter.WriteRawValue), value); public MethodBodyStatement WriteBase64StringValue(ValueExpression value) - => Invoke(nameof(Utf8JsonWriter.WriteBase64StringValue), value).ToStatement(); + => Untyped.Invoke(nameof(Utf8JsonWriter.WriteBase64StringValue), value).ToStatement(); public MethodBodyStatement WriteBinaryData(ValueExpression value) - => new IfElsePreprocessorDirective + => new IfElsePreprocessorStatement ( "NET6_0_OR_GREATER", WriteRawValue(value), - new UsingScopeStatement(typeof(JsonDocument), "document", JsonDocumentExpression.Parse(value), out var jsonDocumentVar) + new UsingScopeStatement(typeof(JsonDocument), "document", JsonDocumentSnippet.Parse(value), out var jsonDocumentVar) { - JsonSerializerExpression.Serialize(this, new JsonDocumentExpression(jsonDocumentVar).RootElement).ToStatement() + Snippet.JsonSerializer.Serialize(this, new JsonDocumentSnippet(jsonDocumentVar).RootElement).ToStatement() } ); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/VariableReferenceSnippet.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/VariableReferenceSnippet.cs new file mode 100644 index 00000000000..8ee9d406a87 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/TypedSnippets/VariableReferenceSnippet.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Generator.CSharp.Expressions; + +namespace Microsoft.Generator.CSharp.Snippets +{ + public sealed record VariableReferenceSnippet(CSharpType Type, CodeWriterDeclaration Declaration) : TypedSnippet(Type, new UntypedVariableReference(Declaration)) + { + public VariableReferenceSnippet(CSharpType type, string name) : this(type, new CodeWriterDeclaration(name)) { } + + private record UntypedVariableReference(CodeWriterDeclaration Declaration) : ValueExpression + { + internal override void Write(CodeWriter writer) + { + writer.Append(Declaration); + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Utilities/FormattableStringHelpers.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Utilities/FormattableStringHelpers.cs index 24addfcfa14..77efa2737eb 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Utilities/FormattableStringHelpers.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Utilities/FormattableStringHelpers.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Diagnostics.CodeAnalysis; +using Microsoft.Generator.CSharp.Providers; namespace Microsoft.Generator.CSharp { @@ -86,10 +87,10 @@ public static bool IsNullOrEmpty(this FormattableString? fs) => public static FormattableString Join(this ICollection fss, string separator, string? lastSeparator = null) => fss.Count == 1 ? fss.First() : Join(fss, fss.Count, static fs => fs, separator, lastSeparator, null); - public static FormattableString GetTypesFormattable(this IReadOnlyCollection parameters) + public static FormattableString GetTypesFormattable(this IReadOnlyCollection parameters) => GetTypesFormattable(parameters, parameters.Count); - public static FormattableString GetTypesFormattable(this IEnumerable parameters, int count) + public static FormattableString GetTypesFormattable(this IEnumerable parameters, int count) => Join(parameters, count, static p => p.Type, ",", null, null); public static string ReplaceLast(this string text, string oldValue, string newValue) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriter.CodeScope.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriter.CodeScope.cs new file mode 100644 index 00000000000..4c0835f7fcd --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriter.CodeScope.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Generator.CSharp +{ + internal sealed partial class CodeWriter + { + public sealed class CodeScope : IDisposable + { + private readonly CodeWriter _writer; + private readonly string? _end; + private readonly bool _newLine; + + internal HashSet Identifiers { get; } = new(); + + internal HashSet AllDefinedIdentifiers { get; } = new(); + + internal List Declarations { get; } = new(); + + internal int Depth { get; } + + internal CodeScope(CodeWriter writer, string? end, bool newLine, int depth) + { + _writer = writer; + _end = end; + _newLine = newLine; + Depth = depth; + } + + public void Dispose() + { + if (_writer != null) + { + _writer.PopScope(this); + foreach (var declaration in Declarations) + { + declaration.SetActualName(null); + } + + Declarations.Clear(); + + if (_end != null) + { + _writer.AppendRaw(_end); + } + + if (_newLine) + { + _writer.WriteLine(); + } + } + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriter.Documentation.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriter.Documentation.cs index e1fb0db5760..48f4fdf38b1 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriter.Documentation.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriter.Documentation.cs @@ -3,25 +3,30 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Text; +using Microsoft.Generator.CSharp.Providers; namespace Microsoft.Generator.CSharp { - public sealed partial class CodeWriter + internal sealed partial class CodeWriter { - public CodeWriter WriteXmlDocumentationSummary(FormattableString? text) + private const string SingleArgFormat = "{0}"; + + public CodeWriter WriteXmlDocumentationSummary(IReadOnlyList lines) { - return WriteXmlDocumentation("summary", text); + return WriteXmlDocumentation("summary", lines); } - public CodeWriter WriteXmlDocumentation(string tag, FormattableString? text) + public CodeWriter WriteXmlDocumentation(string tag, IReadOnlyList lines) { - return WriteDocumentationLines($"<{tag}>", $"", text); + return WriteDocumentationLines($"<{tag}>", $"", lines); } - public CodeWriter WriteXmlDocumentationParameters(IEnumerable parameters) + public CodeWriter WriteXmlDocumentationParameters(IEnumerable parameters) { foreach (var parameter in parameters) { @@ -31,9 +36,9 @@ public CodeWriter WriteXmlDocumentationParameters(IEnumerable paramet return this; } - public CodeWriter WriteXmlDocumentationParameter(string name, FormattableString? text) + public CodeWriter WriteXmlDocumentationParameter(string name, IReadOnlyList lines) { - return WriteDocumentationLines($"", $"", text); + return WriteDocumentationLines($"", $"", lines); } /// @@ -42,19 +47,19 @@ public CodeWriter WriteXmlDocumentationParameter(string name, FormattableString? /// Writer to which code is written to. /// The definition of the parameter, including name and description. /// - public CodeWriter WriteXmlDocumentationParameter(Parameter parameter) + public CodeWriter WriteXmlDocumentationParameter(ParameterProvider parameter) { - return WriteXmlDocumentationParameter(parameter.Name, parameter.Description); + return WriteXmlDocumentationParameter(parameter.Name, [parameter.Description]); } - public CodeWriter WriteXmlDocumentationException(CSharpType exception, FormattableString? description) + public CodeWriter WriteXmlDocumentationException(CSharpType exception, IReadOnlyList lines) { - return WriteDocumentationLines($"", $"", description); + return WriteDocumentationLines($"", $"", lines); } public CodeWriter WriteXmlDocumentationReturns(FormattableString text) { - return WriteDocumentationLines($"", $"", text); + return WriteDocumentationLines($"", $"", [text]); } public CodeWriter WriteXmlDocumentationInclude(string filename, MethodSignature methodSignature, out string memberId) @@ -99,17 +104,17 @@ private static void AppendTypeWithShortNames(CSharpType type, StringBuilder sb) } } - public CodeWriter WriteXmlDocumentationRequiredParametersException(IEnumerable parameters) + public CodeWriter WriteXmlDocumentationRequiredParametersException(IEnumerable parameters) { - return WriteXmlDocumentationParametersExceptions(typeof(ArgumentNullException), parameters.Where(p => p.Validation is ValidationType.AssertNotNull or ValidationType.AssertNotNullOrEmpty).ToArray(), " is null."); + return WriteXmlDocumentationParametersExceptions(typeof(ArgumentNullException), parameters.Where(p => p.Validation is ParameterValidationType.AssertNotNull or ParameterValidationType.AssertNotNullOrEmpty).ToArray(), " is null."); } - public CodeWriter WriteXmlDocumentationNonEmptyParametersException(IEnumerable parameters) + public CodeWriter WriteXmlDocumentationNonEmptyParametersException(IEnumerable parameters) { - return WriteXmlDocumentationParametersExceptions(typeof(ArgumentException), parameters.Where(p => p.Validation == ValidationType.AssertNotNullOrEmpty).ToArray(), " is an empty string, and was expected to be non-empty."); + return WriteXmlDocumentationParametersExceptions(typeof(ArgumentException), parameters.Where(p => p.Validation == ParameterValidationType.AssertNotNullOrEmpty).ToArray(), " is an empty string, and was expected to be non-empty."); } - private CodeWriter WriteXmlDocumentationParametersExceptions(Type exceptionType, IReadOnlyCollection parameters, string reason) + private CodeWriter WriteXmlDocumentationParametersExceptions(Type exceptionType, IReadOnlyCollection parameters, string reason) { if (parameters.Count == 0) { @@ -131,19 +136,14 @@ private CodeWriter WriteXmlDocumentationParametersExceptions(Type exceptionType, formatBuilder.Append(reason); var description = FormattableStringFactory.Create(formatBuilder.ToString(), parameters.Select(p => (object)p.Name).ToArray()); - return WriteXmlDocumentationException(exceptionType, description); + return WriteXmlDocumentationException(exceptionType, [description]); } - public CodeWriter WriteDocumentationLines(FormattableString startTag, FormattableString endTag, FormattableString? text) - => AppendXmlDocumentation(startTag, endTag, text ?? $""); + public CodeWriter WriteDocumentationLines(FormattableString startTag, FormattableString endTag, IReadOnlyList lines) + => AppendXmlDocumentation(startTag, endTag, lines); public CodeWriter WriteMethodDocumentation(MethodSignatureBase methodBase) { - if (methodBase.IsRawSummaryText) - { - return WriteRawXmlDocumentation(methodBase.Description); - } - if (methodBase.NonDocumentComment is { } comment) { WriteLine($"// {comment}"); @@ -151,7 +151,7 @@ public CodeWriter WriteMethodDocumentation(MethodSignatureBase methodBase) if (methodBase.SummaryText is { } summaryText) { - WriteXmlDocumentationSummary(summaryText); + WriteXmlDocumentationSummary([summaryText]); } return WriteMethodDocumentationSignature(methodBase); @@ -161,8 +161,8 @@ public CodeWriter WriteMethodDocumentationSignature(MethodSignatureBase methodBa { WriteXmlDocumentationParameters(methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Public) ? methodBase.Parameters : methodBase.Parameters.Where(p => p.Description is not null)); - WriteXmlDocumentationRequiredParametersException(methodBase.Parameters); - WriteXmlDocumentationNonEmptyParametersException(methodBase.Parameters); + WriteXmlDocumentationRequiredParametersException((IEnumerable)methodBase.Parameters); + WriteXmlDocumentationNonEmptyParametersException((IEnumerable)methodBase.Parameters); if (methodBase is MethodSignature { ReturnDescription: { } } method) { WriteXmlDocumentationReturns(method.ReturnDescription); @@ -176,106 +176,89 @@ public CodeWriter WriteXmlDocumentationInheritDoc(CSharpType? crefType = null) ? WriteLine($"/// ") : WriteLine($"/// "); - internal CodeWriter WriteRawXmlDocumentation(FormattableString? content) - { - if (content is null) - return this; + internal CodeWriter AppendXmlDocumentation(FormattableString startTag, FormattableString endTag, params FormattableString[] lines) + => AppendXmlDocumentation(startTag, endTag, (IReadOnlyList)lines); - var lines = content.ToString().Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - var xmlLines = string.Join('\n', lines.Select(l => "/// " + l)); - AppendRaw(xmlLines); - WriteLine(); - return this; - } - internal CodeWriter AppendXmlDocumentation(FormattableString startTag, FormattableString endTag, FormattableString content) + internal CodeWriter AppendXmlDocumentation(FormattableString startTag, FormattableString endTag, IReadOnlyList lines) { - const string xmlDoc = "/// "; - const string xmlDocNewLine = "\n/// "; - - var commentStart = _length; - AppendRaw(CurrentLine.IsEmpty ? xmlDoc : xmlDocNewLine); + Debug.Assert(!HasNewLines(lines, out var offendingLine), $"'{offendingLine!.ToString()}': contains a newline character. Split this into multiple entries instead."); - var startTagStart = _length; - Append(startTag); _writingXmlDocumentation = true; - var contentStart = _length; - if (content.Format.Length > 0) + if (lines.Count == 0 || IsEmpty(lines)) { - Append(content); + WriteLine($"/// {startTag}{endTag}"); } - var contentEnd = _length; - - _writingXmlDocumentation = false; - Append(endTag); - - if (contentStart == contentEnd) + else if (lines.Count == 1) { - var startTagSpan = WrittenText.Slice(startTagStart + 1, contentStart - startTagStart - 1); - var endTagSpan = WrittenText.Slice(contentEnd + 2); - - if (startTagSpan.SequenceEqual(endTagSpan)) + //should we auto add the '.'? + string lineFormat = lines[0].Format; + string stringToCheck = lineFormat; + if (lineFormat == SingleArgFormat && lines[0].ArgumentCount == 1 && lines[0].GetArgument(0) is string strLine) { - // Remove empty tags - _length = commentStart; + stringToCheck = strLine; } - else + string period = stringToCheck.EndsWith(".") ? string.Empty : "."; + WriteLine($"/// {startTag} {lines[0]}{period} {endTag}"); + } + else + { + WriteLine($"/// {startTag}"); + foreach (var line in lines) { - WriteLine(); + WriteLine($"/// {line}"); } - - return this; + WriteLine($"/// {endTag}"); } - WriteLine(); - var contentSpan = _builder.AsSpan(contentStart, contentEnd - contentStart); + _writingXmlDocumentation = false; - var lastLineBreak = contentSpan.LastIndexOf(_newLine); - if (lastLineBreak == -1) - { - // Add spaces and dot to match existing formatting - if (contentEnd > contentStart) - { - if (contentSpan[^1] != ' ') - { - InsertRaw(contentSpan[^1] == '.' ? " " : ". ", contentEnd); - } - else - { - var trimmedContentSpan = contentSpan.TrimEnd(); - if (trimmedContentSpan[^1] != '.') - { - InsertRaw(".", contentStart + trimmedContentSpan.Length); - } - } + return this; + } - if (contentSpan[0] != ' ') - { - InsertRaw(" ", contentStart); - } - } - return this; + private static bool IsEmpty(IReadOnlyList lines) + { + if (lines.Count != 1) + { + return false; } - if (lastLineBreak != contentSpan.Length) + string lineFormat = lines[0].Format; + if (lineFormat.Equals(string.Empty)) { - InsertRaw(xmlDocNewLine, contentEnd); + return true; } - while (lastLineBreak != -1) + if (lineFormat != SingleArgFormat) { - InsertRaw(xmlDoc, lastLineBreak + contentStart + 1); - contentSpan = contentSpan.Slice(0, lastLineBreak); - lastLineBreak = contentSpan.LastIndexOf(_newLine); + return false; } - if (contentSpan.Length > 0) + var firstArg = lines[0].GetArgument(0); + return firstArg is not null && firstArg.Equals(string.Empty); + } + + private static bool HasNewLines(IReadOnlyList lines, [MaybeNullWhen(false)] out FormattableString offendingLine) + { + offendingLine = null; + foreach (var line in lines) { - InsertRaw(xmlDocNewLine, contentStart); + if (line.Format.Contains(_newLine)) + { + offendingLine = line; + return true; + } + for (int i = 0; i < line.ArgumentCount; i++) + { + if (line.GetArgument(i) is string str && str.Contains(_newLine)) + { + offendingLine = line; + return true; + } + } } - - return this; + return false; } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriter.cs index ef8e1048e7c..3a849bf8b13 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriter.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriter.cs @@ -2,44 +2,44 @@ // Licensed under the MIT License. using System; -using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.Generator.CSharp.Expressions; -using static Microsoft.Generator.CSharp.Expressions.Snippets; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Statements; +using static Microsoft.Generator.CSharp.Snippets.Snippet; namespace Microsoft.Generator.CSharp { - public sealed partial class CodeWriter + internal sealed partial class CodeWriter : IDisposable { - private const int DefaultLength = 1024; - private static readonly string _newLine = "\n"; - private static readonly string _braceNewLine = "{\n"; + private const char _newLine = '\n'; + private const char _space = ' '; private readonly HashSet _usingNamespaces = new HashSet(); - private readonly Stack _scopes; + private readonly Stack _scopes; private string? _currentNamespace; - private char[] _builder; - private int _length; + private UnsafeBufferSequence _builder; private bool _writingXmlDocumentation; + private bool _atBeginningOfLine; internal CodeWriter() { - _builder = ArrayPool.Shared.Rent(DefaultLength); + _builder = new UnsafeBufferSequence(1024); - _scopes = new Stack(); - _scopes.Push(new CodeWriterScope(this, "", false)); + _scopes = new Stack(); + _scopes.Push(new CodeScope(this, "", false, 0)); + _atBeginningOfLine = true; } - public CodeWriterScope Scope(FormattableString line, string start = "{", string end = "}", bool newLine = true, CodeWriterScopeDeclarations? scopeDeclarations = null) + public CodeScope Scope(FormattableString line, string start = "{", string end = "}", bool newLine = true, CodeWriterScopeDeclarations? scopeDeclarations = null) { ValidateDeclarations(scopeDeclarations); - CodeWriterScope codeWriterScope = new CodeWriterScope(this, end, newLine); + CodeScope codeWriterScope = new CodeScope(this, end, newLine, _scopes.Peek().Depth + 1); _scopes.Push(codeWriterScope); WriteLine(line); WriteRawLine(start); @@ -47,7 +47,7 @@ public CodeWriterScope Scope(FormattableString line, string start = "{", string return codeWriterScope; } - public CodeWriterScope Scope() + public CodeScope Scope() { return ScopeRaw(); } @@ -88,15 +88,15 @@ private void AddDeclarationsToScope(CodeWriterScopeDeclarations? scopeDeclaratio } } - private CodeWriterScope ScopeRaw(string start = "{", string end = "}", bool newLine = true) + internal CodeScope ScopeRaw(string start = "{", string end = "}", bool newLine = true) { WriteRawLine(start); - CodeWriterScope codeWriterScope = new CodeWriterScope(this, end, newLine); + CodeScope codeWriterScope = new CodeScope(this, end, newLine, _scopes.Peek().Depth + 1); _scopes.Push(codeWriterScope); return codeWriterScope; } - public CodeWriterScope SetNamespace(string @namespace) + public CodeScope SetNamespace(string @namespace) { _currentNamespace = @namespace; WriteLine($"namespace {@namespace}"); @@ -178,7 +178,7 @@ public CodeWriter Append(FormattableString formattableString) expression.Write(this); break; case var _ when isLiteralFormat: - WriteLiteral(argument); + Literal(argument).Write(this); break; default: string? s = argument?.ToString(); @@ -206,24 +206,32 @@ public CodeWriter Append(FormattableString formattableString) return this; } - /// - /// A wrapper around to allow for writing method body statements. - /// This method will call the extension method of the plugin with the current instance of - /// and attempt to write . - /// - /// The to write. - public void WriteMethod(CSharpMethod method) + public void WriteMethod(MethodProvider method) { - CodeModelPlugin.Instance.CodeWriterExtensionMethods.WriteMethod(this, method); - } + ArgumentNullException.ThrowIfNull(method, nameof(method)); - public void WriteProperty(PropertyDeclaration property) - { - if (!CurrentLine.IsEmpty) + WriteMethodDocumentation(method.Signature); + + if (method.BodyStatements is { } body) { - WriteLine(); + using (WriteMethodDeclaration(method.Signature)) + { + body.Write(this); + } } + else if (method.BodyExpression is { } expression) + { + using (WriteMethodDeclarationNoScope(method.Signature)) + { + AppendRaw(" => "); + expression.Write(this); + WriteRawLine(";"); + } + } + } + public void WriteProperty(PropertyProvider property) + { if (property.Description is not null) { WriteXmlDocumentationSummary(property.Description); @@ -235,7 +243,7 @@ public void WriteProperty(PropertyDeclaration property) { foreach (var (exceptionType, description) in property.Exceptions) { - WriteXmlDocumentationException(exceptionType, description); + WriteXmlDocumentationException(exceptionType, [description]); } } @@ -254,7 +262,7 @@ public void WriteProperty(PropertyDeclaration property) { Append($"{property.ExplicitInterface}."); } - if (property is IndexerDeclaration indexer) + if (property is IndexerProvider indexer) { Append($"{indexer.Name}[{indexer.IndexerParameter.Type} {indexer.IndexerParameter.Name}]"); } @@ -270,9 +278,10 @@ public void WriteProperty(PropertyDeclaration property) AppendRaw(";"); break; case AutoPropertyBody(var hasSetter, var setterModifiers, var initialization): - AppendRaw("{ get;"); + AppendRaw(" { get;"); if (hasSetter) { + AppendRaw(" "); WritePropertyAccessorModifiers(setterModifiers); AppendRaw("set;"); } @@ -285,15 +294,16 @@ public void WriteProperty(PropertyDeclaration property) break; case MethodPropertyBody(var getter, var setter, var setterModifiers): WriteLine(); - WriteRawLine("{"); - // write getter - WriteMethodPropertyAccessor("get", getter); - // write setter - if (setter is not null) + using (ScopeRaw(newLine: false)) { - WriteMethodPropertyAccessor("set", setter, setterModifiers); + // write getter + WriteMethodPropertyAccessor("get", getter); + // write setter + if (setter is not null) + { + WriteMethodPropertyAccessor("set", setter, setterModifiers); + } } - AppendRaw("}"); break; default: throw new InvalidOperationException($"Unhandled property body type {property.Body}"); @@ -304,13 +314,11 @@ public void WriteProperty(PropertyDeclaration property) void WriteMethodPropertyAccessor(string name, MethodBodyStatement body, MethodSignatureModifiers modifiers = MethodSignatureModifiers.None) { WritePropertyAccessorModifiers(modifiers); - WriteRawLine(name); - WriteRawLine("{"); - using (AmbientScope()) + WriteLine($"{name}"); + using (Scope()) { body.Write(this); } - WriteRawLine("}"); } void WritePropertyAccessorModifiers(MethodSignatureModifiers modifiers) @@ -351,16 +359,22 @@ public CodeWriter AppendRawIf(string str, bool condition) return this; } - public void WriteParameter(Parameter clientParameter) + public void WriteParameter(ParameterProvider clientParameter) { if (clientParameter.Attributes.Any()) { AppendRaw("["); - foreach (var attribute in clientParameter.Attributes) + for (int i = 0; i < clientParameter.Attributes.Length; i++) { - Append($"{attribute.Type}, "); + if (i == 0) + { + Append($"{clientParameter.Attributes[i].Type}"); + } + else + { + Append($", {clientParameter.Attributes[i].Type}"); + } } - RemoveTrailingComma(); AppendRaw("]"); } @@ -373,20 +387,13 @@ public void WriteParameter(Parameter clientParameter) AppendRaw(" = "); clientParameter.DefaultValue.Write(this); } - - AppendRaw(","); } - public CodeWriter WriteField(FieldDeclaration field) + public CodeWriter WriteField(FieldProvider field) { - if (!CurrentLine.IsEmpty) - { - WriteLine(); - } - if (field.Description != null) { - WriteXmlDocumentationSummary(field.Description); + WriteXmlDocumentationSummary([field.Description]); } var modifiers = field.Modifiers; @@ -396,7 +403,14 @@ public CodeWriter WriteField(FieldDeclaration field) .AppendRawIf("static ", modifiers.HasFlag(FieldModifiers.Static)) .AppendRawIf("readonly ", modifiers.HasFlag(FieldModifiers.ReadOnly)); - Append($"{field.Type} {field.Name:I}"); + if (field.Declaration.HasBeenDeclared) + { + Append($"{field.Type} {field.Declaration:I}"); + } + else + { + Append($"{field.Type} {field.Declaration:D}"); + } if (field.InitializationValue != null && (modifiers.HasFlag(FieldModifiers.Const) || modifiers.HasFlag(FieldModifiers.Static))) @@ -408,73 +422,19 @@ public CodeWriter WriteField(FieldDeclaration field) return WriteLine($";"); } - public CodeWriter WriteParameterNullChecks(IReadOnlyCollection parameters) + public CodeWriter WriteParametersValidation(IEnumerable parameters) { - foreach (Parameter parameter in parameters) + bool wroteValidation = false; + foreach (ParameterProvider parameter in parameters) { - WriteVariableAssignmentWithNullCheck(parameter.Name, parameter); + MethodBodyStatement validationStatement = Argument.ValidateParameter(parameter); + wroteValidation |= !validationStatement.Equals(EmptyStatement); + validationStatement.Write(this); } - - WriteLine(); - return this; - } - - public void WriteVariableAssignmentWithNullCheck(string variableName, Parameter parameter) - { - var assignToSelf = parameter.Name == variableName; - if (parameter.Initializer != null) - { - if (assignToSelf) - { - WriteLine($"{variableName:I} ??= {parameter.Initializer};"); - } - else - { - WriteLine($"{variableName:I} = {parameter.Name:I} ?? {parameter.Initializer};"); - } - } - else if (parameter.Validation != ValidationType.None) - { - if (assignToSelf) - { - using (Scope($"if ({parameter.Name:I} == null)")) - { - WriteLine($"throw new {typeof(ArgumentNullException)}(nameof({parameter.Name:I}));"); - } - } - else - { - WriteLine($"{variableName:I} = {parameter.Name:I} ?? throw new {typeof(ArgumentNullException)}(nameof({parameter.Name:I}));"); - } - } - else if (!assignToSelf) - { - WriteLine($"{variableName:I} = {parameter.Name:I};"); - } - } - - public CodeWriter WriteParametersValidation(IEnumerable parameters) - { - foreach (Parameter parameter in parameters) - { - WriteParameterValidation(parameter); - } - - WriteLine(); - return this; - } - - private CodeWriter WriteParameterValidation(Parameter parameter) - { - if (parameter.Validation == ValidationType.None && parameter.Initializer != null) + if (wroteValidation) { - return WriteLine($"{parameter.Name:I} ??= {parameter.Initializer};"); + WriteLine(); } - - var validationStatement = Argument.ValidateParameter(parameter); - - validationStatement.Write(this); - return this; } @@ -506,7 +466,7 @@ private bool IsAvailable(string s) } } - foreach (CodeWriterScope codeWriterScope in _scopes) + foreach (CodeScope codeWriterScope in _scopes) { if (codeWriterScope.Identifiers.Contains(s)) { @@ -575,17 +535,28 @@ private void AppendTypeForCRef(CSharpType type) AppendTypeForCRef(argument); } - AppendRaw(","); + if (i < arguments.Count - 1) + AppendRaw(","); } - RemoveTrailingComma(); } } private void AppendType(CSharpType type, bool isDeclaration, bool writeTypeNameOnly) { + if (type.IsArray && type.FrameworkType.GetGenericArguments().Any()) + { + AppendType(type.FrameworkType.GetElementType()!, isDeclaration, writeTypeNameOnly); + AppendRaw("[]"); + return; + } + if (type.TryGetCSharpFriendlyName(out var keywordName)) { AppendRaw(keywordName); + if (type.FrameworkType.IsGenericParameter && type.IsNullable) + { + AppendRaw("?"); + } } else if (isDeclaration && !type.IsFrameworkType) { @@ -608,12 +579,14 @@ private void AppendType(CSharpType type, bool isDeclaration, bool writeTypeNameO if (type.Arguments.Any()) { AppendRaw(_writingXmlDocumentation ? "{" : "<"); - foreach (var typeArgument in type.Arguments) + for (int i = 0; i < type.Arguments.Count; i++) { - AppendType(typeArgument, false, writeTypeNameOnly); - AppendRaw(_writingXmlDocumentation ? "," : ", "); + AppendType(type.Arguments[i], false, writeTypeNameOnly); + if (i != type.Arguments.Count - 1) + { + AppendRaw(_writingXmlDocumentation ? "," : ", "); + } } - RemoveTrailingComma(); AppendRaw(_writingXmlDocumentation ? "}" : ">"); } @@ -623,140 +596,55 @@ private void AppendType(CSharpType type, bool isDeclaration, bool writeTypeNameO } } - public CodeWriter WriteLiteral(object? o) - { - return AppendRaw(o switch - { - null => "null", - string s => SyntaxFactory.Literal(s).ToString(), - int i => SyntaxFactory.Literal(i).ToString(), - long l => SyntaxFactory.Literal(l).ToString(), - decimal d => SyntaxFactory.Literal(d).ToString(), - double d => SyntaxFactory.Literal(d).ToString(), - float f => SyntaxFactory.Literal(f).ToString(), - char c => SyntaxFactory.Literal(c).ToString(), - bool b => b ? "true" : "false", - BinaryData bd => bd.ToArray().Length == 0 ? "new byte[] { }" : SyntaxFactory.Literal(bd.ToString()).ToString(), - _ => throw new NotImplementedException() - }); - } - public CodeWriter WriteLine(FormattableString formattableString) { Append(formattableString); - WriteLine(); - - return this; - } - - public CodeWriter WriteLine() - { - WriteRawLine(string.Empty); - - return this; + return WriteLine(); } - private ReadOnlySpan WrittenText => _builder.AsSpan(0, _length); + public CodeWriter WriteLine() => AppendRawChar(_newLine); - private ReadOnlySpan PreviousLine + public CodeWriter WriteRawLine(string str) { - get - { - var writtenText = WrittenText; - - var indexOfNewLine = writtenText.LastIndexOf(_newLine); - if (indexOfNewLine == -1) - { - return Span.Empty; - } - - var writtenTextBeforeLastLine = writtenText.Slice(0, indexOfNewLine); - var indexOfPreviousNewLine = writtenTextBeforeLastLine.LastIndexOf(_newLine); - if (indexOfPreviousNewLine == -1) - { - return writtenText.Slice(0, indexOfNewLine + 1); - } - - return writtenText.Slice(indexOfPreviousNewLine + 1, indexOfNewLine - indexOfPreviousNewLine); - } + AppendRaw(str); + return WriteLine(); } - private ReadOnlySpan CurrentLine - { - get - { - var writtenText = WrittenText; - - var indexOfNewLine = writtenText.LastIndexOf(_newLine); - if (indexOfNewLine == -1) - { - return writtenText; - } - - return writtenText.Slice(indexOfNewLine + 1); - } - } + public CodeWriter AppendRaw(string str) => AppendRaw(str.AsSpan()); - private void EnsureSpace(int space) + private CodeWriter AppendRawChar(char c) { - if (_builder.Length - _length < space) - { - var newBuilder = ArrayPool.Shared.Rent(Math.Max(_builder.Length + space, _builder.Length * 2)); - _builder.AsSpan().CopyTo(newBuilder); - - ArrayPool.Shared.Return(_builder); - _builder = newBuilder; - } + var destination = _builder.GetSpan(1); + destination[0] = c; + _builder.Advance(1); + _atBeginningOfLine = true; + return this; } - public CodeWriter WriteRawLine(string str) + private CodeWriter AppendRaw(ReadOnlySpan span) { - AppendRaw(str); - - var previousLine = PreviousLine; - - if (CurrentLine.IsEmpty && - (previousLine.SequenceEqual(_newLine) || previousLine.EndsWith(_braceNewLine))) - { + if (span.Length == 0 ) return this; - } - AppendRaw(_newLine); + AddSpaces(); + var destination = _builder.GetSpan(span.Length); + span.CopyTo(destination); + _builder.Advance(span.Length); + + _atBeginningOfLine = span[span.Length - 1] == _newLine; return this; } - public CodeWriter AppendRaw(string str) => AppendRaw(str.AsSpan()); - - private CodeWriter AppendRaw(ReadOnlySpan span) => InsertRaw(span, _length); - - private CodeWriter InsertRaw(ReadOnlySpan span, int position, bool skipNewLineCheck = false) + private void AddSpaces() { - Debug.Assert(0 <= position); - Debug.Assert(position <= _length); - - if (!skipNewLineCheck) - { - var newLineSpan = "\r\n".AsSpan(); - var newLineIndex = span.IndexOf(newLineSpan); - while (newLineIndex != -1) - { - InsertRaw(span.Slice(0, newLineIndex), position, skipNewLineCheck: true); - position += newLineIndex; - span = span.Slice(newLineIndex + 1); - newLineIndex = span.IndexOf(newLineSpan); - } - } - - EnsureSpace(span.Length); - if (position < _length) - { - Array.Copy(_builder, position, _builder, span.Length + position, _length - position); - } + int spaces = _atBeginningOfLine ? (_scopes.Peek().Depth) * 4 : 0; + if (spaces == 0) + return; - span.CopyTo(_builder.AsSpan(position)); - _length += span.Length; - return this; + var destination = _builder.GetSpan(spaces); + destination.Slice(0, spaces).Fill(_space); + _builder.Advance(spaces); } internal CodeWriter WriteIdentifier(string identifier) @@ -806,25 +694,9 @@ public IDisposable WriteMethodDeclaration(MethodSignatureBase methodBase, params public IDisposable WriteMethodDeclarationNoScope(MethodSignatureBase methodBase, params string[] disabledWarnings) { - if (methodBase.Attributes is { } attributes) + foreach (var attribute in methodBase.Attributes) { - foreach (var attribute in attributes) - { - if (attribute.Arguments.Any()) - { - Append($"[{attribute.Type}("); - foreach (var argument in attribute.Arguments) - { - argument.Write(this); - } - RemoveTrailingComma(); - WriteRawLine(")]"); - } - else - { - WriteLine($"[{attribute.Type}]"); - } - } + attribute.Write(this); } foreach (var disabledWarning in disabledWarnings) @@ -845,13 +717,26 @@ public IDisposable WriteMethodDeclarationNoScope(MethodSignatureBase methodBase, .AppendRawIf("new ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.New)) .AppendRawIf("async ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Async)); - if (method.ReturnType != null) + var isImplicitOrExplicit = methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Implicit) || methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Explicit); + if (!isImplicitOrExplicit) { - Append($"{method.ReturnType} "); + if (method.ReturnType != null) + { + Append($"{method.ReturnType} "); + } + else + { + AppendRaw("void "); + } } - else + + AppendRawIf("implicit ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Implicit)) + .AppendRawIf("explicit ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Explicit)) + .AppendRawIf("operator ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Operator)); + + if (isImplicitOrExplicit) { - AppendRaw("void "); + Append($"{method.ReturnType}"); } if (method.ExplicitInterface is not null) @@ -864,11 +749,14 @@ public IDisposable WriteMethodDeclarationNoScope(MethodSignatureBase methodBase, if (method?.GenericArguments != null) { AppendRaw("<"); - foreach (var argument in method.GenericArguments) + for (int i = 0; i < method.GenericArguments.Count; i++) { - Append($"{argument:D},"); + Append($"{method.GenericArguments[i]}"); + if (i != method.GenericArguments.Count - 1) + { + AppendRaw(", "); + } } - RemoveTrailingComma(); AppendRaw(">"); } } @@ -882,21 +770,25 @@ public IDisposable WriteMethodDeclarationNoScope(MethodSignatureBase methodBase, var outerScope = AmbientScope(); - foreach (var parameter in methodBase.Parameters) + for (int i = 0; i < methodBase.Parameters.Count; i++) { - WriteParameter(parameter); + WriteParameter(methodBase.Parameters[i]); + if (i != methodBase.Parameters.Count - 1) + { + AppendRaw(", "); + } } - - RemoveTrailingComma(); Append($")"); if (methodBase is MethodSignature { GenericParameterConstraints: { } constraints }) { - WriteLine(); - foreach (var constraint in constraints) + using (ScopeRaw(string.Empty, string.Empty, false)) { - constraint.Write(this); - AppendRaw(" "); + foreach (var constraint in constraints) + { + constraint.Write(this); + AppendRaw(" "); + } } } @@ -907,12 +799,16 @@ public IDisposable WriteMethodDeclarationNoScope(MethodSignatureBase methodBase, if (!isBase || arguments.Any()) { AppendRaw(isBase ? ": base(" : ": this("); - foreach (var argument in arguments) + var iterator = arguments.GetEnumerator(); + if (iterator.MoveNext()) { - argument.Write(this); - AppendRaw(", "); + iterator.Current.Write(this); + while (iterator.MoveNext()) + { + AppendRaw(", "); + iterator.Current.Write(this); + } } - RemoveTrailingComma(); AppendRaw(")"); } } @@ -933,17 +829,18 @@ public override string ToString() public string ToString(bool header) { - if (_length == 0) - { + var reader = _builder.ExtractReader(); + var totalLength = reader.Length; + if (totalLength == 0) return string.Empty; - } - var builder = new StringBuilder(_length); + + var builder = new StringBuilder((int)totalLength); IEnumerable namespaces = _usingNamespaces .OrderByDescending(ns => ns.StartsWith("System")) .ThenBy(ns => ns, StringComparer.Ordinal); if (header) { - string licenseString = CodeModelPlugin.Instance.CodeWriterExtensionMethods.LicenseString; + string licenseString = CodeModelPlugin.Instance.LicenseString; if (!string.IsNullOrEmpty(licenseString)) { builder.Append(licenseString); @@ -968,120 +865,19 @@ public string ToString(bool header) } } - // Normalize newlines - var spanLines = _builder.AsSpan(0, _length).EnumerateLines(); - int lineCount = 0; - foreach (var line in spanLines) - { - builder.Append(line.TrimEnd()); - builder.Append(_newLine); - lineCount++; - } - // Remove last new line if there are more than 1 - if (lineCount > 1) - { - builder.Remove(builder.Length - _newLine.Length, _newLine.Length); - } + reader.CopyTo(builder, default); return builder.ToString(); } - public sealed class CodeWriterScope : IDisposable - { - private readonly CodeWriter _writer; - private readonly string? _end; - private readonly bool _newLine; - - internal HashSet Identifiers { get; } = new(); - - internal HashSet AllDefinedIdentifiers { get; } = new(); - - internal List Declarations { get; } = new(); - - internal CodeWriterScope(CodeWriter writer, string? end, bool newLine) - { - _writer = writer; - _end = end; - _newLine = newLine; - } - - public void Dispose() - { - if (_writer != null) - { - _writer.PopScope(this); - foreach (var declaration in Declarations) - { - declaration.SetActualName(null); - } - - Declarations.Clear(); - - if (_end != null) - { - _writer.TrimNewLines(); - _writer.AppendRaw(_end); - } - - if (_newLine) - { - _writer.WriteLine(); - } - } - } - } - - private void TrimNewLines() - { - while (PreviousLine.SequenceEqual(_newLine) && - CurrentLine.IsEmpty) - { - _length--; - } - } - - private void PopScope(CodeWriterScope expected) + private void PopScope(CodeScope expected) { var actual = _scopes.Pop(); Debug.Assert(actual == expected); } - private int? FindLastNonWhitespaceCharacterIndex() - { - var text = WrittenText; - for (int i = text.Length - 1; i >= 0; i--) - { - if (char.IsWhiteSpace(text[i])) - { - continue; - } - - return i; - } - - return null; - } - - public void RemoveTrailingCharacter() - { - int? lastCharIndex = FindLastNonWhitespaceCharacterIndex(); - if (lastCharIndex.HasValue) - { - _length = lastCharIndex.Value; - } - } - - public void RemoveTrailingComma() + public CodeScope AmbientScope() { - int? lastCharIndex = FindLastNonWhitespaceCharacterIndex(); - if (lastCharIndex.HasValue && WrittenText[lastCharIndex.Value] == ',') - { - _length = lastCharIndex.Value; - } - } - - public CodeWriterScope AmbientScope() - { - var codeWriterScope = new CodeWriterScope(this, null, false); + var codeWriterScope = new CodeScope(this, null, false, _scopes.Peek().Depth); _scopes.Push(codeWriterScope); return codeWriterScope; } @@ -1096,6 +892,7 @@ internal void WriteTypeModifiers(TypeSignatureModifiers modifiers) AppendRawIf("public ", modifiers.HasFlag(TypeSignatureModifiers.Public)) .AppendRawIf("internal ", modifiers.HasFlag(TypeSignatureModifiers.Internal)) .AppendRawIf("private ", modifiers.HasFlag(TypeSignatureModifiers.Private)) + .AppendRawIf("readonly ", modifiers.HasFlag(TypeSignatureModifiers.ReadOnly)) .AppendRawIf("static ", modifiers.HasFlag(TypeSignatureModifiers.Static)) .AppendRawIf("sealed ", modifiers.HasFlag(TypeSignatureModifiers.Sealed)) .AppendRawIf("partial ", modifiers.HasFlag(TypeSignatureModifiers.Partial)); // partial must be the last to write otherwise compiler will complain @@ -1114,12 +911,16 @@ public void WriteTypeArguments(IEnumerable? typeArguments) } AppendRaw("<"); - foreach (var argument in typeArguments) + var iterator = typeArguments.GetEnumerator(); + if (iterator.MoveNext()) { - Append($"{argument}, "); + Append($"{iterator.Current}"); + while (iterator.MoveNext()) + { + AppendRaw(", "); + Append($"{iterator.Current}"); + } } - - RemoveTrailingComma(); AppendRaw(">"); } @@ -1128,29 +929,42 @@ public void WriteArguments(IEnumerable arguments, bool useSingl if (useSingleLine) { AppendRaw("("); - foreach (var argument in arguments) + var iterator = arguments.GetEnumerator(); + if (iterator.MoveNext()) { - argument.Write(this); - AppendRaw(", "); + iterator.Current.Write(this); + while (iterator.MoveNext()) + { + AppendRaw(", "); + iterator.Current.Write(this); + } } - - RemoveTrailingComma(); AppendRaw(")"); } else { WriteRawLine("("); - foreach (var argument in arguments) + var iterator = arguments.GetEnumerator(); + if (iterator.MoveNext()) { AppendRaw("\t"); - argument.Write(this); + iterator.Current.Write(this); WriteRawLine(","); + while (iterator.MoveNext()) + { + AppendRaw(", "); + AppendRaw("\t"); + iterator.Current.Write(this); + WriteRawLine(","); + } } - - RemoveTrailingCharacter(); - RemoveTrailingComma(); AppendRaw(")"); } } + + public void Dispose() + { + _builder?.Dispose(); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriterDeclaration.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriterDeclaration.cs index 7dfcb31d15a..7682052ade1 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriterDeclaration.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriterDeclaration.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -10,6 +10,8 @@ public sealed class CodeWriterDeclaration private string? _actualName; private string? _debuggerName; + public bool HasBeenDeclared => _actualName != null; + public CodeWriterDeclaration(string name) { RequestedName = name; diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriterExtensionMethods.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriterExtensionMethods.cs deleted file mode 100644 index 13b9fdcb875..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/CodeWriterExtensionMethods.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Microsoft.Generator.CSharp -{ - /// - /// Contains extension methods for . - /// - public class CodeWriterExtensionMethods - { - /// - /// The license string for the generated code to be written. - /// - public virtual string LicenseString => string.Empty; - - /// - /// Writes the given method to the writer. A valid instance of is required. - /// - /// The instance to write to. - /// The to write. - public virtual void WriteMethod(CodeWriter writer, CSharpMethod method) - { - ArgumentNullException.ThrowIfNull(writer, nameof(writer)); - ArgumentNullException.ThrowIfNull(method, nameof(method)); - - writer.WriteMethodDocumentation(method.Signature); - - if (method.BodyStatements is { } body) - { - using (writer.WriteMethodDeclaration(method.Signature)) - { - body.Write(writer); - } - } - else if (method.BodyExpression is { } expression) - { - using (writer.WriteMethodDeclarationNoScope(method.Signature)) - { - writer.AppendRaw(" => "); - expression.Write(writer); - writer.WriteRawLine(";"); - } - } - - writer.WriteLine(); - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/DiagnosticScope.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/DiagnosticScope.cs index 7d936305010..e872d5faa4f 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/DiagnosticScope.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/DiagnosticScope.cs @@ -1,17 +1,17 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; namespace Microsoft.Generator.CSharp { - public class DiagnosticScope : IDisposable + internal class DiagnosticScope : IDisposable { - private readonly CodeWriter.CodeWriterScope _scope; + private readonly CodeWriter.CodeScope _scope; private readonly CodeWriterDeclaration _scopeVariable; private readonly CodeWriter _writer; - public DiagnosticScope(CodeWriter.CodeWriterScope scope, CodeWriterDeclaration scopeVariable, CodeWriter writer) + public DiagnosticScope(CodeWriter.CodeScope scope, CodeWriterDeclaration scopeVariable, CodeWriter writer) { _scope = scope; _scopeVariable = scopeVariable; diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/TypeProviderWriter.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/TypeProviderWriter.cs index 39bdf173766..13f04e19059 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/TypeProviderWriter.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/TypeProviderWriter.cs @@ -2,147 +2,185 @@ // Licensed under the MIT License. using System.Linq; +using Microsoft.Generator.CSharp.Providers; namespace Microsoft.Generator.CSharp { public class TypeProviderWriter { protected readonly TypeProvider _provider; - protected readonly CodeWriter _writer; - public TypeProviderWriter(CodeWriter writer, TypeProvider provider) + public TypeProviderWriter(TypeProvider provider) { _provider = provider; - _writer = writer; } - public virtual void Write() + public virtual CodeFile Write() { - using (_writer.SetNamespace(_provider.Namespace)) + using var writer = new CodeWriter(); + return Write(writer); + } + + private CodeFile Write(CodeWriter writer) + { + using (writer.SetNamespace(_provider.Namespace)) { - WriteType(); + WriteType(writer); } + return new CodeFile(writer.ToString(), _provider.FileName); } - private void WriteType() + private void WriteType(CodeWriter writer) { - _writer.WriteTypeModifiers(_provider.DeclarationModifiers); // class, struct, enum and interface is written as modifiers in this part - _writer.Append($"{_provider.Type:D}") + writer.WriteTypeModifiers(_provider.DeclarationModifiers); // class, struct, enum and interface is written as modifiers in this part + writer.Append($"{_provider.Type:D}") .AppendRawIf(" : ", _provider.Inherits != null || _provider.Implements.Any()) .AppendIf($"{_provider.Inherits},", _provider.Inherits != null); - foreach (var implement in _provider.Implements) + for (int i = 0; i < _provider.Implements.Count; i++) { - _writer.Append($"{implement:D},"); + writer.Append($"{_provider.Implements[i]:D}"); + if (i < _provider.Implements.Count - 1) + { + writer.AppendRaw(", "); + } } - _writer.RemoveTrailingComma(); if (_provider.WhereClause is not null) { - _provider.WhereClause.Write(_writer); + using (writer.ScopeRaw(string.Empty, string.Empty, false)) + { + _provider.WhereClause.Write(writer); + } } - _writer.WriteLine(); + writer.WriteLine(); if (_provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Enum)) { - WriteEnumContent(); + WriteEnumContent(writer); } else if (_provider.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Interface)) { - WriteInterfaceContent(); + WriteInterfaceContent(writer); } else { - WriteClassOrStructContent(); + WriteClassOrStructContent(writer); } } - private void WriteClassOrStructContent() + private void WriteClassOrStructContent(CodeWriter writer) { - using (_writer.Scope()) + using (writer.Scope()) { - WriteFields(); - - WriteConstructors(); - - WriteProperties(); - WriteMethods(); - - WriteNestedTypes(); + bool sectionWritten = _provider.Fields.Any(); + WriteFields(writer); + + if (sectionWritten && _provider.Constructors.Any()) + writer.WriteLine(); + WriteConstructors(writer); + sectionWritten |= _provider.Constructors.Any(); + + if (sectionWritten && _provider.Properties.Any()) + writer.WriteLine(); + WriteProperties(writer); + sectionWritten |= _provider.Properties.Any(); + + if (sectionWritten && _provider.Methods.Any()) + writer.WriteLine(); + WriteMethods(writer); + sectionWritten |= _provider.Methods.Any(); + + if (sectionWritten = _provider.NestedTypes.Any()) + writer.WriteLine(); + WriteNestedTypes(writer); } } - private void WriteEnumContent() + private void WriteEnumContent(CodeWriter writer) { - using (_writer.Scope()) + using (writer.Scope()) { - foreach (var field in _provider.Fields) + for (int i = 0; i < _provider.Fields.Count; i++) { - _writer.Append($"{field.Name}"); - if (field.InitializationValue != null) + writer.Append($"{_provider.Fields[i].Name}"); + if (_provider.Fields[i].InitializationValue != null) { - _writer.AppendRaw(" = "); - field.InitializationValue.Write(_writer); + writer.AppendRaw(" = "); + _provider.Fields[i].InitializationValue!.Write(writer); + } + if (i < _provider.Fields.Count - 1) + { + writer.WriteRawLine(","); } - _writer.WriteRawLine(","); } - _writer.RemoveTrailingComma(); + writer.WriteLine(); } } - private void WriteInterfaceContent() + private void WriteInterfaceContent(CodeWriter writer) { - using (_writer.Scope()) + using (writer.Scope()) { // temporarily do nothing until we have a requirement for writing interfaces: https://github.com/microsoft/typespec/issues/3442 } } - protected virtual void WriteProperties() + private void WriteProperties(CodeWriter writer) { - foreach (var property in _provider.Properties) + for (int i = 0; i < _provider.Properties.Count; i++) { - _writer.WriteProperty(property); - _writer.WriteLine(); + writer.WriteProperty(_provider.Properties[i]); + if (i < _provider.Properties.Count - 1) + { + writer.WriteLine(); + } } - _writer.WriteLine(); } - protected virtual void WriteFields() + private void WriteFields(CodeWriter writer) { - foreach (var field in _provider.Fields) + for (int i = 0; i < _provider.Fields.Count; i++) { - _writer.WriteField(field); + writer.WriteField(_provider.Fields[i]); } - _writer.WriteLine(); } - protected virtual void WriteConstructors() + private void WriteConstructors(CodeWriter writer) { - foreach (var ctor in _provider.Constructors) + for (int i = 0; i < _provider.Constructors.Count; i++) { - _writer.WriteMethod(ctor); + writer.WriteMethod(_provider.Constructors[i]); + if (i < _provider.Constructors.Count - 1) + { + writer.WriteLine(); + } } - _writer.WriteLine(); } - protected virtual void WriteMethods() + private void WriteMethods(CodeWriter writer) { - foreach (var method in _provider.Methods) + for (int i = 0; i < _provider.Methods.Count; i++) { - _writer.WriteMethod(method); + writer.WriteMethod(_provider.Methods[i]); + if (i < _provider.Methods.Count - 1) + { + writer.WriteLine(); + } } - _writer.WriteLine(); } - protected virtual void WriteNestedTypes() + private void WriteNestedTypes(CodeWriter writer) { - foreach (var nested in _provider.NestedTypes) + for (int i = 0; i < _provider.NestedTypes.Count; i++) { - var nestedWriter = new TypeProviderWriter(_writer, nested); - nestedWriter.Write(); - _writer.WriteLine(); + var nestedWriter = new TypeProviderWriter(_provider.NestedTypes[i]); + nestedWriter.Write(writer); + if (i < _provider.NestedTypes.Count - 1) + { + writer.WriteLine(); + } } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/UnsafeBufferSegment.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/UnsafeBufferSegment.cs new file mode 100644 index 00000000000..5ce764e9df5 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/UnsafeBufferSegment.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Generator.CSharp; + +internal struct UnsafeBufferSegment +{ + public char[] Array; + public int Written; +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/UnsafeBufferSequence.Reader.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/UnsafeBufferSequence.Reader.cs new file mode 100644 index 00000000000..222beb2d341 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/UnsafeBufferSequence.Reader.cs @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Buffers; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Generator.CSharp; + +/// +/// Provides a way to read a without exposing the underlying buffers. +/// This class is not thread safe and should only be used by one thread at a time. +/// If you dispose while another thread is copying you will end up with a partial copy. +/// +internal partial class UnsafeBufferSequence +{ + //Only needed to restrict ctor access of Reader to BufferSequence + private class ReaderInstance : Reader + { + public ReaderInstance(UnsafeBufferSegment[] buffers, int count) + : base(buffers, count) + { + } + } + + internal class Reader : IDisposable + { + private UnsafeBufferSegment[] _buffers; + private int _count; + private bool _isDisposed; + private static readonly object _disposeLock = new object(); + + private protected Reader(UnsafeBufferSegment[] buffers, int count) + { + _buffers = buffers; + _count = count; + } + + public long Length + { + get + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(Reader)); + } + + long result = 0; + for (int i = 0; i < _count; i++) + { + result += _buffers[i].Written; + } + return result; + } + } + + public void CopyTo(Stream stream, CancellationToken cancellation) + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(Reader)); + } + + for (int i = 0; i < _count; i++) + { + cancellation.ThrowIfCancellationRequested(); + + UnsafeBufferSegment buffer = _buffers[i]; + byte[] bufferToWrite = Encoding.UTF8.GetBytes(buffer.Array, 0, buffer.Written); + stream.Write(bufferToWrite, 0, bufferToWrite.Length); + } + } + + public void CopyTo(StringBuilder builder, CancellationToken cancellation) + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(Reader)); + } + + for (int i = 0; i < _count; i++) + { + cancellation.ThrowIfCancellationRequested(); + + UnsafeBufferSegment buffer = _buffers[i]; + builder.Append(buffer.Array, 0, buffer.Written); + } + } + + public async Task CopyToAsync(Stream stream, CancellationToken cancellation) + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(Reader)); + } + + for (int i = 0; i < _count; i++) + { + cancellation.ThrowIfCancellationRequested(); + + UnsafeBufferSegment buffer = _buffers[i]; + byte[] bufferToWrite = Encoding.UTF8.GetBytes(buffer.Array, 0, buffer.Written); + await stream.WriteAsync(bufferToWrite, 0, bufferToWrite.Length, cancellation).ConfigureAwait(false); + } + } + + public BinaryData ToBinaryData() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(Reader)); + } + + long length = Length; + if (length > int.MaxValue) + { + throw new InvalidOperationException($"Length of serialized model is too long. Value was {length} max is {int.MaxValue}"); + } + using var stream = new MemoryStream((int)length); + CopyTo(stream, default); + return new BinaryData(stream.GetBuffer().AsMemory(0, (int)stream.Position)); + } + + public void Dispose() + { + if (!_isDisposed) + { + lock (_disposeLock) + { + if (!_isDisposed) + { + int buffersToReturnCount = _count; + var buffersToReturn = _buffers; + _count = 0; + _buffers = Array.Empty(); + for (int i = 0; i < buffersToReturnCount; i++) + { + ArrayPool.Shared.Return(buffersToReturn[i].Array); + } + _isDisposed = true; + } + } + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/UnsafeBufferSequence.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/UnsafeBufferSequence.cs new file mode 100644 index 00000000000..143b7e1e536 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Writers/UnsafeBufferSequence.cs @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Buffers; + +namespace Microsoft.Generator.CSharp; + +/// +/// This class is a helper to write to a in a thread safe manner. +/// It uses the shared pool to allocate buffers and returns them to the pool when disposed. +/// Since there is no way to ensure someone didn't keep a reference to one of the buffers +/// it must be disposed of in the same context it was created and its referenced should not be stored or shared. +/// +internal sealed partial class UnsafeBufferSequence : IBufferWriter, IDisposable +{ + private volatile UnsafeBufferSegment[] _buffers; // this is an array so items can be accessed by ref + private volatile int _count; + private readonly int _segmentSize; + + /// + /// Initializes a new instance of . + /// + /// The size of each buffer segment. + public UnsafeBufferSequence(int segmentSize = 16384) + { + // we perf tested a very large and a small model and found that the performance + // for 4k, 8k, 16k, 32k, was negligible for the small model but had a 30% alloc improvement + // from 4k to 16k on the very large model. + _segmentSize = segmentSize; + _buffers = Array.Empty(); + } + + /// + /// Notifies the that bytes bytes were written to the output or . + /// You must request a new buffer after calling to continue writing more data; you cannot write to a previously acquired buffer. + /// + /// The number of bytes written to the or . + /// + public void Advance(int bytesWritten) + { + ref UnsafeBufferSegment last = ref _buffers[_count - 1]; + last.Written += bytesWritten; + if (last.Written > last.Array.Length) + { + throw new ArgumentOutOfRangeException(nameof(bytesWritten)); + } + } + + /// + /// Returns a to write to that is at least the requested size, as specified by the parameter. + /// + /// The minimum length of the returned . If less than 256, a buffer of size 256 will be returned. + /// A memory buffer of at least bytes. If is less than 256, a buffer of size 256 will be returned. + public Memory GetMemory(int sizeHint = 0) + { + if (sizeHint < 256) + { + sizeHint = 256; + } + + int sizeToRent = sizeHint > _segmentSize ? sizeHint : _segmentSize; + + if (_buffers.Length == 0) + { + ExpandBuffers(sizeToRent); + } + + ref UnsafeBufferSegment last = ref _buffers[_count - 1]; + Memory free = last.Array.AsMemory(last.Written); + if (free.Length >= sizeHint) + { + return free; + } + + // else allocate a new buffer: + ExpandBuffers(sizeToRent); + + return _buffers[_count - 1].Array; + } + + private readonly object _lock = new object(); + private void ExpandBuffers(int sizeToRent) + { + lock (_lock) + { + int bufferCount = _count == 0 ? 1 : _count * 2; + + UnsafeBufferSegment[] resized = new UnsafeBufferSegment[bufferCount]; + if (_count > 0) + { + _buffers.CopyTo(resized, 0); + } + _buffers = resized; + _buffers[_count].Array = ArrayPool.Shared.Rent(sizeToRent); + _count = bufferCount == 1 ? bufferCount : _count + 1; + } + } + + /// + /// Returns a to write to that is at least the requested size, as specified by the parameter. + /// + /// The minimum length of the returned . If less than 256, a buffer of size 256 will be returned. + /// A buffer of at least bytes. If is less than 256, a buffer of size 256 will be returned. + public Span GetSpan(int sizeHint = 0) + { + Memory memory = GetMemory(sizeHint); + return memory.Span; + } + + /// + /// Disposes the SequenceWriter and returns the underlying buffers to the pool. + /// + public void Dispose() + { + int bufferCountToFree; + UnsafeBufferSegment[] buffersToFree; + lock (_lock) + { + bufferCountToFree = _count; + buffersToFree = _buffers; + _count = 0; + _buffers = Array.Empty(); + } + + for (int i = 0; i < bufferCountToFree; i++) + { + ArrayPool.Shared.Return(buffersToFree[i].Array); + } + } + + public Reader ExtractReader() + { + lock (_lock) + { + Reader reader = new ReaderInstance(_buffers, _count); + _count = 0; + _buffers = Array.Empty(); + return reader; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ArgumentTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ArgumentTests.cs new file mode 100644 index 00000000000..d4a7e3acd49 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ArgumentTests.cs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using UnbrandedTypeSpec; + +namespace Microsoft.Generator.CSharp.Tests +{ + public class ArgumentTests + { + [TestCase("test")] + public void NotNull(object? value) + { + Argument.AssertNotNull(value, "value"); + + Assert.AreEqual("test", value!.ToString()); + } + + [Test] + public void NotNullThrowsOnNull() + { + object? value = null; + Assert.Throws(() => Argument.AssertNotNull(value, "value")); + } + + [Test] + public void NotNullNullableInt32() + { + int? value = 1; + Argument.AssertNotNull(value, "value"); + } + + [Test] + public void NotNullNullableInt32ThrowsOnNull() + { + int? value = null; + Assert.Throws(() => Argument.AssertNotNull(value, "value")); + } + + [Test] + public void NotNullOrEmptyCollection() + { + string[] value = { "test" }; + Argument.AssertNotNullOrEmpty(value, "value"); + } + + [Test] + public void NotNullOrEmptyCollectionThrowsOnNull() + { + string[]? value = null; + Assert.Throws(() => Argument.AssertNotNullOrEmpty(value, "value")); + } + + [TestCaseSource(nameof(GetNotNullOrEmptyCollectionThrowsOnEmptyCollectionData))] + public void NotNullOrEmptyCollectionThrowsOnEmptyCollection(IEnumerable? value) + { + Assert.Throws(() => Argument.AssertNotNullOrEmpty(value, "value")); + } + + [Test] + public void NotNullOrEmptyString() + { + string value = "test"; + Argument.AssertNotNullOrEmpty(value, "value"); + } + + [Test] + public void NotNullOrEmptyStringThrowsOnNull() + { + string? value = null; + Assert.Throws(() => Argument.AssertNotNullOrEmpty(value, "value")); + } + + [Test] + public void NotNullOrEmptyStringThrowsOnEmpty() + { + Assert.Throws(() => Argument.AssertNotNullOrEmpty(string.Empty, "value")); + } + + [Test] + public void NotNullOrWhiteSpace() + { + string value = "test"; + Argument.AssertNotNullOrWhiteSpace(value, "value"); + } + + [Test] + public void NotNullOrWhiteSpaceThrowsOnNull() + { + Assert.Throws(() => Argument.AssertNotNullOrWhiteSpace(null, "value")); + } + + [Test] + public void NotNullOrWhiteSpaceThrowsOnEmpty() + { + Assert.Throws(() => Argument.AssertNotNullOrWhiteSpace(string.Empty, "value")); + } + + [Test] + public void NotNullOrWhiteSpaceThrowsOnWhiteSpace() + { + Assert.Throws(() => Argument.AssertNotNullOrWhiteSpace(string.Empty, " ")); + } + + [Test] + public void NotDefault() + { + TestStructure value = new TestStructure("test", 1); + Argument.AssertNotDefault(ref value, "value"); + } + + [Test] + public void NotDefaultThrows() + { + TestStructure value = default; + Assert.Throws(() => Argument.AssertNotDefault(ref value, "value")); + } + + [TestCase(0, 0, 2)] + [TestCase(1, 0, 2)] + [TestCase(2, 0, 2)] + public void InRangeInt32(int value, int minimum, int maximum) + { + Argument.AssertInRange(value, minimum, maximum, "value"); + } + + [TestCase(-1, 0, 2)] + [TestCase(3, 0, 2)] + public void InRangeInt32Throws(int value, int minimum, int maximum) + { + Assert.Throws(() => Argument.AssertInRange(value, minimum, maximum, "value")); + } + + [Test] + public void CheckNotNull() + { + var value = "test"; + var checkedValue = Argument.CheckNotNull(value, "value"); + + Assert.AreEqual(value, checkedValue); + } + + [Test] + public void CheckNotNullOrEmptyString() + { + string value = "test"; + var checkedValue = Argument.CheckNotNullOrEmpty(value, "value"); + + Assert.AreEqual(value, checkedValue); + } + + [Test] + public void CheckNotNullOrEmptyStringThrowsOnNull() + { + string? value = null; + Assert.Throws(() => Argument.CheckNotNullOrEmpty(value, "value")); + } + + [Test] + public void CheckNotNullOrEmptyStringThrowsOnEmpty() + { + Assert.Throws(() => Argument.CheckNotNullOrEmpty(string.Empty, "value")); + } + + private readonly struct TestStructure : IEquatable + { + internal readonly string A; + internal readonly int B; + + internal TestStructure(string a, int b) + { + A = a; + B = b; + } + + public bool Equals(TestStructure other) => string.Equals(A, other.A, StringComparison.Ordinal) && B == other.B; + } + + private static IEnumerable> GetNotNullOrEmptyCollectionThrowsOnEmptyCollectionData() + { + static IEnumerable NotNullOrEmptyCollectionThrowsOnEmptyCollection() + { + yield break; + } + + yield return new string[0]; + yield return NotNullOrEmptyCollectionThrowsOnEmptyCollection(); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CSharpGenTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CSharpGenTests.cs index 090c9b0ffc6..adc0583e4ec 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CSharpGenTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CSharpGenTests.cs @@ -4,11 +4,11 @@ using System; using System.Collections.Generic; using System.IO; -using Microsoft.Generator.CSharp.Expressions; using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Snippets; using Moq; using NUnit.Framework; -using static Microsoft.Generator.CSharp.Expressions.ExtensibleSnippets; +using static Microsoft.Generator.CSharp.Snippets.ExtensibleSnippets; namespace Microsoft.Generator.CSharp.Tests { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CSharpMethodCollectionTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CSharpMethodCollectionTests.cs index 411cbddb0c9..dd08e09b9e9 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CSharpMethodCollectionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CSharpMethodCollectionTests.cs @@ -6,6 +6,7 @@ using System.IO; using System.Reflection; using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Providers; using Moq; using NUnit.Framework; @@ -22,7 +23,7 @@ internal class CSharpMethodCollectionTests [SetUp] public void Setup() { - var mockParameter = new Parameter("mockParam", null, typeof(bool), null, ValidationType.None, null); + var mockParameter = new ParameterProvider("mockParam", $"mock description", typeof(bool), null); var mockTypeFactory = new Mock() { }; mockTypeFactory.Setup(t => t.CreateCSharpType(It.IsAny())).Returns(new CSharpType(typeof(bool))); mockTypeFactory.Setup(t => t.CreateCSharpParam(It.IsAny())).Returns(mockParameter); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CSharpTypeTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CSharpTypeTests.cs index e1f568fe087..e5cf8aed3e1 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CSharpTypeTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CSharpTypeTests.cs @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.Collections.Generic; using System; -using NUnit.Framework; -using System.Linq; +using System.Collections.Generic; using System.Collections.Immutable; -using Moq; using System.IO; +using System.Linq; using System.Text; +using Moq; +using NUnit.Framework; namespace Microsoft.Generator.CSharp.Tests { @@ -302,45 +302,6 @@ public void IsCollectionType(Type type, bool expected) Assert.AreEqual(expected, actual); } - [Test] - public void InitializationType_ReadOnlyMemory() - { - var arguments = typeof(int); - var cSharpType = new CSharpType(typeof(ReadOnlyMemory<>), arguments: arguments); - var actual = cSharpType.InitializationType; - var expected = new CSharpType(arguments.MakeArrayType()); - - var areEqual = actual.Equals(expected); - - Assert.IsTrue(areEqual); - } - - [Test] - public void InitializationType_List() - { - var arguments = typeof(int); - var listType = new CSharpType(typeof(IList<>), arguments: arguments); - var actual = listType.InitializationType; - var expected = new CSharpType(typeof(List<>), arguments: arguments); - - var areEqual = actual.Equals(expected); - - Assert.IsTrue(areEqual); - } - - [Test] - public void InitializationType_Dictionary() - { - var arguments = new CSharpType[] { typeof(string), typeof(int) }; - var cSharpType = new CSharpType(typeof(IDictionary<,>), arguments: arguments); - var actual = cSharpType.InitializationType; - var expected = new CSharpType(typeof(Dictionary<,>), arguments: arguments); - - var areEqual = actual.Equals(expected); - - Assert.IsTrue(areEqual); - } - [Test] public void PropertyInitializationType_ReadOnlyMemory() { @@ -428,10 +389,76 @@ public void TestToString(Type type, string expectedString) var cSharpType = new CSharpType(type); var actual = cSharpType.ToString(); var expected = new StringBuilder() - .Append(expectedString).Append(CodeWriterTests.NewLine) + .Append(expectedString) .ToString(); Assert.AreEqual(expected, actual); } + + [TestCaseSource(nameof(ValidateNullableTypesData))] + public void ValidateNullableTypes(Type type, IReadOnlyList expectedArguments, bool expectedIsNullable) + { + var csharpType = new CSharpType(type); + + CollectionAssert.AreEqual(expectedArguments, csharpType.Arguments); + Assert.AreEqual(expectedIsNullable, csharpType.IsNullable); + } + + private static object[] ValidateNullableTypesData = [ + new object[] + { + typeof(int), Array.Empty(), false + }, + new object[] + { + typeof(int?), Array.Empty(), true + }, + new object[] + { + typeof(Uri), Array.Empty(), false + }, + new object[] + { + typeof(Guid), Array.Empty(), false + }, + new object[] + { + typeof(Guid?), Array.Empty(), true + }, + new object[] + { + typeof(TestStruct), new CSharpType[] { typeof(int) }, false + }, + new object[] + { + typeof(TestStruct?), new CSharpType[] { typeof(int) }, true + }, + new object[] + { + typeof(TestStruct), new CSharpType[] { typeof(int?) }, false + }, + new object[] + { + typeof(TestStruct?), new CSharpType[] { typeof(int?) }, true + }, + new object[] + { + typeof(TestStruct>), new CSharpType[] { typeof(TestStruct) }, false + }, + new object[] + { + typeof(TestStruct>?), new CSharpType[] { typeof(TestStruct) }, true + }, + new object[] + { + typeof(TestStruct?>), new CSharpType[] { typeof(TestStruct?) }, false + }, + new object[] + { + typeof(TestStruct?>?), new CSharpType[] { typeof(TestStruct?) }, true + }, + ]; + + internal struct TestStruct { } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ChangeTrackingDictionaryTest.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ChangeTrackingDictionaryTest.cs new file mode 100644 index 00000000000..67a9941f3b5 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ChangeTrackingDictionaryTest.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using UnbrandedTypeSpec; +using NUnit.Framework; + +namespace Microsoft.Generator.CSharp.Tests +{ + public class ChangeTrackingDictionaryTest + { + [Test] + public void UndefinedByDefault() + { + var dictionary = new ChangeTrackingDictionary(); + Assert.True(dictionary.IsUndefined); + } + + [Test] + public void ReadOperationsDoNotChange() + { + var dictionary = new ChangeTrackingDictionary(); + _ = dictionary.Count; + _ = dictionary.IsReadOnly; + _ = dictionary.Keys; + _ = dictionary.Values; + _ = dictionary.Contains(new KeyValuePair("c", "d")); + _ = dictionary.ContainsKey("a"); + _ = dictionary.TryGetValue("a", out _); + _ = dictionary.Remove("a"); + + foreach (var kvp in dictionary) + { + } + + dictionary.CopyTo(new KeyValuePair[5], 0); + + Assert.Throws(() => _ = dictionary["a"]); + + Assert.True(dictionary.IsUndefined); + } + + [Test] + public void CanAddElement() + { + var dictionary = new ChangeTrackingDictionary(); + dictionary.Add("a", "b"); + + Assert.AreEqual("b", dictionary["a"]); + Assert.False(dictionary.IsUndefined); + } + + [Test] + public void RemoveElement() + { + var dictionary = new ChangeTrackingDictionary(); + dictionary.Add("a", "b"); + dictionary.Add(new KeyValuePair("c", "d")); + dictionary.Remove("a"); + dictionary.Remove(new KeyValuePair("c", "d")); + + Assert.AreEqual(0, dictionary.Count); + Assert.False(dictionary.IsUndefined); + } + + [Test] + public void ClearResetsUndefined() + { + var dictionary = new ChangeTrackingDictionary(); + dictionary.Clear(); + + Assert.AreEqual(0, dictionary.Count); + Assert.False(dictionary.IsUndefined); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ChangeTrackingListTest.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ChangeTrackingListTest.cs new file mode 100644 index 00000000000..b01eee3f001 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ChangeTrackingListTest.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using UnbrandedTypeSpec; +using NUnit.Framework; + +namespace Microsoft.Generator.CSharp.Tests +{ + public class ChangeTrackingListTest + { + [Test] + public void UndefinedByDefault() + { + var list = new ChangeTrackingList(); + Assert.True(list.IsUndefined); + } + + [Test] + public void ReadOperationsDoNotChange() + { + var list = new ChangeTrackingList(); + _ = list.Count; + _ = list.IsReadOnly; + _ = list.Contains("a"); + _ = list.IndexOf("a"); + _ = list.Remove("a"); + + foreach (var kvp in list) + { + } + + list.CopyTo(new string[5], 0); + + Assert.Throws(() => _ = list[0]); + + Assert.True(list.IsUndefined); + } + + [Test] + public void CanAddElement() + { + var list = new ChangeTrackingList(); + list.Add("a"); + + Assert.AreEqual("a", list[0]); + Assert.False(list.IsUndefined); + } + + [Test] + public void CanInsertElement() + { + var list = new ChangeTrackingList(); + list.Insert(0, "a"); + + Assert.AreEqual("a", list[0]); + Assert.False(list.IsUndefined); + } + + [Test] + public void ContainsWorks() + { + var list = new ChangeTrackingList(); + list.Add("a"); + + Assert.True(list.Contains("a")); + } + + [Test] + public void CanEnumerateItems() + { + var list = new ChangeTrackingList(); + list.Add("a"); + + Assert.AreEqual(new[] { "a" }, list.ToArray()); + } + + [Test] + public void RemoveElement() + { + var list = new ChangeTrackingList(); + list.Add("a"); + list.Remove("a"); + + Assert.AreEqual(0, list.Count); + Assert.False(list.IsUndefined); + } + + [Test] + public void ClearResetsUndefined() + { + var list = new ChangeTrackingList(); + list.Clear(); + + Assert.AreEqual(0, list.Count); + Assert.False(list.IsUndefined); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CodeWriterExtensionTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CodeWriterExtensionTests.cs index 50e72355de7..c7630a0168e 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CodeWriterExtensionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CodeWriterExtensionTests.cs @@ -2,12 +2,15 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; +using System.IO; using System.Text; -using Moq; using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Snippets; +using Microsoft.Generator.CSharp.Statements; +using Moq; using NUnit.Framework; -using System.Collections.Generic; -using System.IO; namespace Microsoft.Generator.CSharp.Tests { @@ -47,7 +50,7 @@ public void Setup() [Test] public void NoExtensionMethods() { - var writer = new CodeWriter(); + using var writer = new CodeWriter(); Assert.IsNotNull(writer); } @@ -57,13 +60,13 @@ public void NoExtensionMethods() [TestCase(typeof(string), 22, "((string)22)")] public void TestWriteValueExpression_DefaultCastExpression(Type type, object inner, string expectedWritten) { - var castExpression = new CastExpression(Snippets.Literal(inner), type); - var codeWriter = new CodeWriter(); + var castExpression = new CastExpression(Snippet.Literal(inner), type); + using var codeWriter = new CodeWriter(); castExpression.Write(codeWriter); var sb = new StringBuilder(); sb.Append(_header); - sb.Append(expectedWritten).Append(CodeWriterTests.NewLine); + sb.Append(expectedWritten); Assert.AreEqual(sb.ToString(), codeWriter.ToString()); } @@ -73,12 +76,12 @@ public void TestWriteValueExpression_DefaultCastExpression(Type type, object inn public void TestWriteValueExpression_CustomExpression() { var mockCastExpression = new MockExpression(); - var codeWriter = new CodeWriter(); + using var codeWriter = new CodeWriter(); mockCastExpression.Write(codeWriter); var sb = new StringBuilder(); sb.Append(_header); - sb.Append("Custom implementation").Append(CodeWriterTests.NewLine); + sb.Append("Custom implementation"); Assert.AreEqual(sb.ToString(), codeWriter.ToString()); } @@ -88,59 +91,21 @@ public void TestWriteValueExpression_CustomExpression() [TestCase("bar", "{ \"bar\" }")] public void TestWriteValueExpression_DefaultCollectionInitializerExpression(string literal, string expectedWritten) { - var stringLiteralExpression = new StringLiteralExpression(literal, false); + var stringLiteralExpression = Snippet.Literal(literal); CollectionInitializerExpression expression = new CollectionInitializerExpression(stringLiteralExpression); - var codeWriter = new CodeWriter(); + using var codeWriter = new CodeWriter(); expression.Write(codeWriter); var sb = new StringBuilder(); sb.Append(_header); - sb.Append(expectedWritten).Append(CodeWriterTests.NewLine); + sb.Append(expectedWritten); Assert.AreEqual(sb.ToString(), codeWriter.ToString()); } - // This test validates that the WriteMethod method works as expected using the extension method implementation in the mock code model plugin. - // A mock method with a body constructed from body statements is supplied. - [Test] - public void TestWriteMethodWithBodyStatements() - { - var method = ConstructMockMethod(); - var codeWriter = new CodeWriter(); - - codeWriter.WriteMethod(method); - - var result = codeWriter.ToString(); - var expected = new StringBuilder() - .Append(_header) - .Append("Custom implementation").Append(CodeWriterTests.NewLine) - .ToString(); - - Assert.AreEqual(expected, result); - - } - - // This test validates that the WriteMethod method works as expected given a mock method with a body - // constructed from body expressions using the custom implementation of the extension methods in a mock code model plugin. - [Test] - public void TestWriteMethodWithBodyExpressions() - { - var method = ConstructMockMethod(); - var codeWriter = new CodeWriter(); - codeWriter.WriteMethod(method); - - var result = codeWriter.ToString(); - var expected = new StringBuilder() - .Append(_header) - .Append("Custom implementation").Append(CodeWriterTests.NewLine) - .ToString(); - - Assert.AreEqual(expected, result); - } - // Construct a mock method with a body. The body can be either a list of statements or a single expression // depending on the value of the useExpressionAsBody parameter. - private static CSharpMethod ConstructMockMethod() + private static MethodProvider ConstructMockMethod() { // create method signature var methodName = "TestMethod"; @@ -149,20 +114,20 @@ private static CSharpMethod ConstructMockMethod() FormattableString returnDescription = $"Sample return description for {methodName}"; var methodSignatureModifiers = MethodSignatureModifiers.Public; var returnType = new CSharpType(typeof(BinaryData)); - var parameters = new List() + var parameters = new List() { - new Parameter("param1", $"Sample description for param1", new CSharpType(typeof(string)), null, Validation: ValidationType.AssertNotNullOrEmpty, Initializer: null) + new ParameterProvider("param1", $"Sample description for param1", new CSharpType(typeof(string))) { Validation = ParameterValidationType.AssertNotNull } }; - var responseVar = new VariableReference(returnType, "responseParamName"); - var responseRef = Snippets.Var(responseVar, BinaryDataExpression.FromBytes(new StringLiteralExpression("sample response", false))); + var responseVar = new VariableReferenceSnippet(returnType, "responseParamName"); + var responseRef = Snippet.Var(responseVar, BinaryDataSnippet.FromBytes(Snippet.Literal("sample response"))); var resultStatements = new List() { responseRef, new KeywordStatement("return", responseVar) }; - var method = new CSharpMethod + var method = new MethodProvider ( new MethodSignature(methodName, summary, description, methodSignatureModifiers, returnType, returnDescription, parameters), resultStatements, diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CodeWriterTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CodeWriterTests.cs index 27795d8cba2..383b359dd72 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CodeWriterTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CodeWriterTests.cs @@ -7,8 +7,9 @@ using System.Linq; using System.Text; using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Providers; using NUnit.Framework; -using static Microsoft.Generator.CSharp.Expressions.Snippets; +using static Microsoft.Generator.CSharp.Snippets.Snippet; namespace Microsoft.Generator.CSharp.Tests { @@ -42,6 +43,12 @@ public void Setup() _ = new MockCodeModelPlugin(new GeneratorContext(Configuration.Load(configFilePath))); } + [TearDown] + public void TearDown() + { + _codeWriter?.Dispose(); + } + [Test] public void GeneratesNewNamesInChildScope() { @@ -57,7 +64,7 @@ public void GeneratesNewNamesInChildScope() sb.Append(_header); sb.Append("a").Append(NewLine); sb.Append("{").Append(NewLine); - sb.Append("a0").Append(NewLine); + sb.Append(" a0").Append(NewLine); sb.Append("}").Append(NewLine); Assert.AreEqual(sb.ToString(), _codeWriter.ToString()); @@ -78,11 +85,14 @@ public void ScopeLineIsInsideScope() var sb = new StringBuilder(); sb.Append(_header); - sb.Append("a").Append(NewLine); - sb.Append("{").Append(NewLine); + //TODO strange behavior for scope that we might want to fix. + // if you want the "a" and "{" lines to be the same indent level as "}" + // you must write A then use an empty `Scope()` method call. + sb.Append(" a").Append(NewLine); + sb.Append(" {").Append(NewLine); sb.Append("}").Append(NewLine); - sb.Append("a").Append(NewLine); - sb.Append("{").Append(NewLine); + sb.Append(" a").Append(NewLine); + sb.Append(" {").Append(NewLine); sb.Append("}").Append(NewLine); Assert.AreEqual(sb.ToString(), _codeWriter.ToString()); @@ -103,7 +113,7 @@ public void VariableNameNotReusedWhenUsedInChildScope() var sb = new StringBuilder(); sb.Append(_header); sb.Append("{").Append(NewLine); - sb.Append("a").Append(NewLine); + sb.Append(" a").Append(NewLine); sb.Append("}").Append(NewLine); sb.Append("a0").Append(NewLine); @@ -114,7 +124,7 @@ public void VariableNameNotReusedWhenUsedInChildScope() public void CorrectlyHandlesCurlyBraces() { _codeWriter.Append($"public {typeof(string)} Data {{ get; private set; }}"); - var expected = "public string Data { get; private set; }" + NewLine; + var expected = "public string Data { get; private set; }"; var sb = new StringBuilder(); sb.Append(_header); sb.Append(expected); @@ -130,7 +140,7 @@ public void FormatInFormat() FormattableString fs2 = $"'a' is {typeof(char)} and {fs1} and 'true' is {typeof(bool)}"; _codeWriter.Append(fs2); - var expected = "'a' is char and '1' is int and 'true' is bool" + NewLine; + var expected = "'a' is char and '1' is int and 'true' is bool"; var sb = new StringBuilder(); sb.Append(_header); sb.Append(expected); @@ -143,7 +153,7 @@ public void FormatInFormat() public void EnumerableFormatInFormat() { _codeWriter.Append($"Multiply:{Enumerable.Range(1, 4).Select(i => (FormattableString)$" {i} * 2 = {i * 2};")}"); - var expected = "Multiply: 1 * 2 = 2; 2 * 2 = 4; 3 * 2 = 6; 4 * 2 = 8;" + NewLine; + var expected = "Multiply: 1 * 2 = 2; 2 * 2 = 4; 3 * 2 = 6; 4 * 2 = 8;"; var sb = new StringBuilder(); sb.Append(_header); sb.Append(expected); @@ -154,7 +164,7 @@ public void EnumerableFormatInFormat() [Test] public void SingleLineSummary() { - _codeWriter.WriteXmlDocumentationSummary($"Some {typeof(string)} summary."); + _codeWriter.WriteXmlDocumentationSummary([$"Some {typeof(string)} summary."]); var expected = "/// Some string summary. " + NewLine; var sb = new StringBuilder(); sb.Append(_header); @@ -166,12 +176,12 @@ public void SingleLineSummary() [Test] public void NoEmptySummary() { - _codeWriter.WriteXmlDocumentationSummary($"{string.Empty}"); - var expected = string.Empty; + _codeWriter.WriteXmlDocumentationSummary([$"{string.Empty}"]); + var expected = "/// " + NewLine; var sb = new StringBuilder(); sb.Append(expected); - Assert.AreEqual(sb.ToString(), _codeWriter.ToString()); + Assert.AreEqual(sb.ToString(), _codeWriter.ToString(false)); } [TestCase(typeof(string), false, "", "")] @@ -183,7 +193,7 @@ public void NoEmptySummary() public void SeeCRefType(Type type, bool isNullable, string expectedWritten, string ns) { var csType = new CSharpType(type).WithNullable(isNullable); - _codeWriter.WriteXmlDocumentationSummary($"Some {csType:C} summary."); + _codeWriter.WriteXmlDocumentationSummary([$"Some {csType:C} summary."]); var expected = $"/// Some {expectedWritten} summary. " + NewLine; var sb = new StringBuilder(); sb.Append(_header); @@ -202,26 +212,40 @@ public void SeeCRefType(Type type, bool isNullable, string expectedWritten, stri [Test] public void MultiLineSummary() { - FormattableString fs1 = $@"L04 -L05 -L06 {typeof(int)} - + List fs1 = new List + { + $"L04", + $"L05", + $"L06 {typeof(int)}", + $"", + $"", + $"L09" + }; -L09"; - FormattableString fs2 = $@" + List fs2 = new List + { + $"", + $"L11 {typeof(bool)}", + $"L12", + $"", + $"" + }; -L11 {typeof(bool)} -L12 + List fss = new List(); + fss.AddRange(fs1); + fss.AddRange(fs2); -"; - IEnumerable fss = new[] { fs1, fs2 }; - FormattableString fs = $@"L00 -L01 -L02 {typeof(string)} + List fs = new List() + { + $"L00", + $"L01", + $"L02 {typeof(string)}", + $"" + }; + fs.AddRange(fss); + fs.Add($"L15"); + fs.Add($"L16"); -{fss} -L15 -L16"; _codeWriter.WriteXmlDocumentationSummary(fs); var sb = new StringBuilder(); @@ -230,18 +254,18 @@ public void MultiLineSummary() sb.Append("/// L00").Append(NewLine); sb.Append("/// L01").Append(NewLine); sb.Append("/// L02 string").Append(NewLine); - sb.Append("///").Append(NewLine); + sb.Append("/// ").Append(NewLine); sb.Append("/// L04").Append(NewLine); sb.Append("/// L05").Append(NewLine); sb.Append("/// L06 int").Append(NewLine); - sb.Append("///").Append(NewLine); - sb.Append("///").Append(NewLine); + sb.Append("/// ").Append(NewLine); + sb.Append("/// ").Append(NewLine); sb.Append("/// L09").Append(NewLine); - sb.Append("///").Append(NewLine); + sb.Append("/// ").Append(NewLine); sb.Append("/// L11 bool").Append(NewLine); sb.Append("/// L12").Append(NewLine); - sb.Append("///").Append(NewLine); - sb.Append("///").Append(NewLine); + sb.Append("/// ").Append(NewLine); + sb.Append("/// ").Append(NewLine); sb.Append("/// L15").Append(NewLine); sb.Append("/// L16").Append(NewLine); sb.Append("/// ").Append(NewLine); @@ -253,15 +277,15 @@ public void MultiLineSummary() [Test] public void TestWriteMethodDeclarationNoScope_ConstructorSignature() { - var baseInitializerStatement = new ConstructorInitializer(true, new List { new StringLiteralExpression("test", false) }); + var baseInitializerStatement = new ConstructorInitializer(true, new List { Literal("test") }); var constructorSignature = new ConstructorSignature(new CSharpType(typeof(string)), $"Test constructor summary", $"Test description", - MethodSignatureModifiers.Public, Array.Empty(), null, baseInitializerStatement); - var codeWriter = new CodeWriter(); + MethodSignatureModifiers.Public, Array.Empty(), null, baseInitializerStatement); + using var codeWriter = new CodeWriter(); codeWriter.WriteMethodDeclarationNoScope(constructorSignature); var expected = new StringBuilder() .Append(_header) - .Append("public String(): base(\"test\")").Append(NewLine) + .Append("public String(): base(\"test\")") .ToString(); var result = codeWriter.ToString(); Assert.AreEqual(expected, result); @@ -270,16 +294,13 @@ public void TestWriteMethodDeclarationNoScope_ConstructorSignature() [Test] public void CodeWriter_WriteField() { - var field1 = new FieldDeclaration($"To test int", FieldModifiers.Private, typeof(int), "_intConst"); - var field2 = new FieldDeclaration($"To test string", FieldModifiers.Private | FieldModifiers.Static | FieldModifiers.ReadOnly, typeof(string), "_stringValue"); - var field3 = new FieldDeclaration($"To test a field with initialization value", FieldModifiers.Private | FieldModifiers.Static | FieldModifiers.ReadOnly, typeof(string), "withValue") - { - InitializationValue = Literal("abc") - }; + var field1 = new FieldProvider(FieldModifiers.Private, typeof(int), "_intConst", $"To test int"); + var field2 = new FieldProvider(FieldModifiers.Private | FieldModifiers.Static | FieldModifiers.ReadOnly, typeof(string), "_stringValue", $"To test string"); + var field3 = new FieldProvider(FieldModifiers.Private | FieldModifiers.Static | FieldModifiers.ReadOnly, typeof(string), "withValue", $"To test a field with initialization value", Literal("abc")); - var codeWriter = new CodeWriter(); + using var codeWriter = new CodeWriter(); codeWriter.WriteField(field1); - codeWriter.Append($"// test comment"); + codeWriter.WriteLine($"// test comment"); codeWriter.WriteField(field2); codeWriter.WriteField(field3); @@ -302,14 +323,14 @@ public void CodeWriter_WriteField() [Test] public void CodeWriter_WriteProperty_AutoBody() { - var property1 = new PropertyDeclaration($"To test an auto property without a setter", MethodSignatureModifiers.Public, typeof(string), "Property1", new AutoPropertyBody(false)); - var property2 = new PropertyDeclaration($"To test an auto property with a setter", MethodSignatureModifiers.Public, typeof(string), "Property2", new AutoPropertyBody(true, MethodSignatureModifiers.None)); - var property3 = new PropertyDeclaration($"To test an auto property with an internal setter", MethodSignatureModifiers.Public, typeof(string), "Property3", new AutoPropertyBody(true, MethodSignatureModifiers.Internal)); - var property4 = new PropertyDeclaration($"To test an auto property with an internal setter and initialization value", MethodSignatureModifiers.Public, typeof(string), "Property4", new AutoPropertyBody(true, MethodSignatureModifiers.Internal, Literal("abc"))); + var property1 = new PropertyProvider($"To test an auto property without a setter", MethodSignatureModifiers.Public, typeof(string), "Property1", new AutoPropertyBody(false)); + var property2 = new PropertyProvider($"To test an auto property with a setter", MethodSignatureModifiers.Public, typeof(string), "Property2", new AutoPropertyBody(true, MethodSignatureModifiers.None)); + var property3 = new PropertyProvider($"To test an auto property with an internal setter", MethodSignatureModifiers.Public, typeof(string), "Property3", new AutoPropertyBody(true, MethodSignatureModifiers.Internal)); + var property4 = new PropertyProvider($"To test an auto property with an internal setter and initialization value", MethodSignatureModifiers.Public, typeof(string), "Property4", new AutoPropertyBody(true, MethodSignatureModifiers.Internal, Literal("abc"))); - var codeWriter = new CodeWriter(); + using var codeWriter = new CodeWriter(); codeWriter.WriteProperty(property1); - codeWriter.Append($"// test comment"); + codeWriter.WriteLine($"// test comment"); codeWriter.WriteProperty(property2); codeWriter.WriteProperty(property3); codeWriter.WriteProperty(property4); @@ -317,14 +338,14 @@ public void CodeWriter_WriteProperty_AutoBody() var expected = new StringBuilder() .Append(_header) .Append("/// To test an auto property without a setter. ").Append(NewLine) - .Append("public string Property1{ get; }").Append(NewLine) + .Append("public string Property1 { get; }").Append(NewLine) .Append("// test comment").Append(NewLine) .Append("/// To test an auto property with a setter. ").Append(NewLine) - .Append("public string Property2{ get;set; }").Append(NewLine) + .Append("public string Property2 { get; set; }").Append(NewLine) .Append("/// To test an auto property with an internal setter. ").Append(NewLine) - .Append("public string Property3{ get;internal set; }").Append(NewLine) + .Append("public string Property3 { get; internal set; }").Append(NewLine) .Append("/// To test an auto property with an internal setter and initialization value. ").Append(NewLine) - .Append("public string Property4{ get;internal set; } = \"abc\";").Append(NewLine) + .Append("public string Property4 { get; internal set; } = \"abc\";").Append(NewLine) .ToString(); var result = codeWriter.ToString(); @@ -335,13 +356,13 @@ public void CodeWriter_WriteProperty_AutoBody() [Test] public void CodeWriter_WriteProperty_AutoBody_WithExplicitInterface() { - var property1 = new PropertyDeclaration($"To test an auto property without a setter", MethodSignatureModifiers.Public, typeof(int), nameof(IList.Count), new AutoPropertyBody(false), ExplicitInterface: typeof(IList)); - var property2 = new PropertyDeclaration($"To test an auto property with a setter", MethodSignatureModifiers.Public, typeof(bool), nameof(IList.IsReadOnly), new AutoPropertyBody(true, MethodSignatureModifiers.None), ExplicitInterface: typeof(IList)); - var property3 = new PropertyDeclaration($"To test an auto property with an internal setter", MethodSignatureModifiers.Public, typeof(int), nameof(IReadOnlyList.Count), new AutoPropertyBody(true, MethodSignatureModifiers.Internal), ExplicitInterface: typeof(IReadOnlyList)); + var property1 = new PropertyProvider($"To test an auto property without a setter", MethodSignatureModifiers.Public, typeof(int), nameof(IList.Count), new AutoPropertyBody(false), explicitInterface: typeof(IList)); + var property2 = new PropertyProvider($"To test an auto property with a setter", MethodSignatureModifiers.Public, typeof(bool), nameof(IList.IsReadOnly), new AutoPropertyBody(true, MethodSignatureModifiers.None), explicitInterface: typeof(IList)); + var property3 = new PropertyProvider($"To test an auto property with an internal setter", MethodSignatureModifiers.Public, typeof(int), nameof(IReadOnlyList.Count), new AutoPropertyBody(true, MethodSignatureModifiers.Internal), explicitInterface: typeof(IReadOnlyList)); - var codeWriter = new CodeWriter(); + using var codeWriter = new CodeWriter(); codeWriter.WriteProperty(property1); - codeWriter.Append($"// test comment"); + codeWriter.WriteLine($"// test comment"); codeWriter.WriteProperty(property2); codeWriter.WriteProperty(property3); @@ -349,12 +370,12 @@ public void CodeWriter_WriteProperty_AutoBody_WithExplicitInterface() .Append(_header) .Append("using System.Collections.Generic;").Append(NewLine).Append(NewLine) .Append("/// To test an auto property without a setter. ").Append(NewLine) - .Append("public int global::System.Collections.Generic.IList.Count{ get; }").Append(NewLine) + .Append("public int global::System.Collections.Generic.IList.Count { get; }").Append(NewLine) .Append("// test comment").Append(NewLine) .Append("/// To test an auto property with a setter. ").Append(NewLine) - .Append("public bool global::System.Collections.Generic.IList.IsReadOnly{ get;set; }").Append(NewLine) + .Append("public bool global::System.Collections.Generic.IList.IsReadOnly { get; set; }").Append(NewLine) .Append("/// To test an auto property with an internal setter. ").Append(NewLine) - .Append("public int global::System.Collections.Generic.IReadOnlyList.Count{ get;internal set; }").Append(NewLine) + .Append("public int global::System.Collections.Generic.IReadOnlyList.Count { get; internal set; }").Append(NewLine) .ToString(); var result = codeWriter.ToString(); @@ -365,12 +386,12 @@ public void CodeWriter_WriteProperty_AutoBody_WithExplicitInterface() [Test] public void CodeWriter_WriteProperty_ExpressionBody() { - var property1 = new PropertyDeclaration($"To test an expression property with string type", MethodSignatureModifiers.Public, typeof(string), "Property1", new ExpressionPropertyBody(Literal("abc"))); - var property2 = new PropertyDeclaration($"To test an expression property with int type", MethodSignatureModifiers.Public, typeof(int), "Property2", new ExpressionPropertyBody(Literal(299792458))); + var property1 = new PropertyProvider($"To test an expression property with string type", MethodSignatureModifiers.Public, typeof(string), "Property1", new ExpressionPropertyBody(Literal("abc"))); + var property2 = new PropertyProvider($"To test an expression property with int type", MethodSignatureModifiers.Public, typeof(int), "Property2", new ExpressionPropertyBody(Literal(299792458))); - var codeWriter = new CodeWriter(); + using var codeWriter = new CodeWriter(); codeWriter.WriteProperty(property1); - codeWriter.Append($"// test comment"); + codeWriter.WriteLine($"// test comment"); codeWriter.WriteProperty(property2); var expected = new StringBuilder() @@ -390,10 +411,10 @@ public void CodeWriter_WriteProperty_ExpressionBody() [Test] public void CodeWriter_WriteProperty_ExpressionBody_WithExplicitInterface() { - var property1 = new PropertyDeclaration($"To test an expression property with int type", MethodSignatureModifiers.Public, typeof(int), nameof(IList.Count), new ExpressionPropertyBody(Literal(299792458)), ExplicitInterface: typeof(IList)); + var property1 = new PropertyProvider($"To test an expression property with int type", MethodSignatureModifiers.Public, typeof(int), nameof(IList.Count), new ExpressionPropertyBody(Literal(299792458)), explicitInterface: typeof(IList)); - var codeWriter = new CodeWriter(); - codeWriter.Append($"// test comment"); + using var codeWriter = new CodeWriter(); + codeWriter.WriteLine($"// test comment"); codeWriter.WriteProperty(property1); var expected = new StringBuilder() @@ -412,13 +433,13 @@ public void CodeWriter_WriteProperty_ExpressionBody_WithExplicitInterface() [Test] public void CodeWriter_WriteProperty_MethodPropertyBody() { - var property1 = new PropertyDeclaration($"To test an auto property without a setter", MethodSignatureModifiers.Public, typeof(string), "Property1", new MethodPropertyBody(Return(Literal("abc")))); - var property2 = new PropertyDeclaration($"To test an auto property with a setter", MethodSignatureModifiers.Public, typeof(string), "Property2", new MethodPropertyBody(Return(Literal("abc")), Assign(new FormattableStringToExpression($"Property2"), new KeywordExpression("value", null)))); - var property3 = new PropertyDeclaration($"To test an auto property with an internal setter", MethodSignatureModifiers.Public, typeof(string), "Property3", new MethodPropertyBody(Return(Literal("abc")), Assign(new FormattableStringToExpression($"Property3"), new KeywordExpression("value", null)), MethodSignatureModifiers.Internal)); + var property1 = new PropertyProvider($"To test an auto property without a setter", MethodSignatureModifiers.Public, typeof(string), "Property1", new MethodPropertyBody(Return(Literal("abc")))); + var property2 = new PropertyProvider($"To test an auto property with a setter", MethodSignatureModifiers.Public, typeof(string), "Property2", new MethodPropertyBody(Return(Literal("abc")), Assign(This.Property("Property2"), new KeywordExpression("value", null)))); + var property3 = new PropertyProvider($"To test an auto property with an internal setter", MethodSignatureModifiers.Public, typeof(string), "Property3", new MethodPropertyBody(Return(Literal("abc")), Assign(This.Property("Property3"), new KeywordExpression("value", null)), MethodSignatureModifiers.Internal)); - var codeWriter = new CodeWriter(); + using var codeWriter = new CodeWriter(); codeWriter.WriteProperty(property1); - codeWriter.Append($"// test comment"); + codeWriter.WriteLine($"// test comment"); codeWriter.WriteProperty(property2); codeWriter.WriteProperty(property3); @@ -427,35 +448,35 @@ public void CodeWriter_WriteProperty_MethodPropertyBody() .Append("/// To test an auto property without a setter. ").Append(NewLine) .Append("public string Property1").Append(NewLine) .Append("{").Append(NewLine) - .Append("get").Append(NewLine) - .Append("{").Append(NewLine) - .Append("return \"abc\";").Append(NewLine) - .Append("}").Append(NewLine) + .Append(" get").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" return \"abc\";").Append(NewLine) + .Append(" }").Append(NewLine) .Append("}").Append(NewLine) .Append("// test comment").Append(NewLine) .Append("/// To test an auto property with a setter. ").Append(NewLine) .Append("public string Property2").Append(NewLine) .Append("{").Append(NewLine) - .Append("get").Append(NewLine) - .Append("{").Append(NewLine) - .Append("return \"abc\";").Append(NewLine) - .Append("}").Append(NewLine) - .Append("set").Append(NewLine) - .Append("{").Append(NewLine) - .Append("Property2 = value;").Append(NewLine) - .Append("}").Append(NewLine) + .Append(" get").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" return \"abc\";").Append(NewLine) + .Append(" }").Append(NewLine) + .Append(" set").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" this.Property2 = value;").Append(NewLine) + .Append(" }").Append(NewLine) .Append("}").Append(NewLine) .Append("/// To test an auto property with an internal setter. ").Append(NewLine) .Append("public string Property3").Append(NewLine) .Append("{").Append(NewLine) - .Append("get").Append(NewLine) - .Append("{").Append(NewLine) - .Append("return \"abc\";").Append(NewLine) - .Append("}").Append(NewLine) - .Append("internal set").Append(NewLine) - .Append("{").Append(NewLine) - .Append("Property3 = value;").Append(NewLine) - .Append("}").Append(NewLine) + .Append(" get").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" return \"abc\";").Append(NewLine) + .Append(" }").Append(NewLine) + .Append(" internal set").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" this.Property3 = value;").Append(NewLine) + .Append(" }").Append(NewLine) .Append("}").Append(NewLine) .ToString(); @@ -467,13 +488,13 @@ public void CodeWriter_WriteProperty_MethodPropertyBody() [Test] public void CodeWriter_WriteProperty_MethodPropertyBody_WithExplicitInterface() { - var property1 = new PropertyDeclaration($"To test an auto property without a setter", MethodSignatureModifiers.Public, typeof(int), nameof(IList.Count), new MethodPropertyBody(Return(Literal(299792458))), ExplicitInterface: typeof(IList)); - var property2 = new PropertyDeclaration($"To test an auto property with a setter", MethodSignatureModifiers.Public, typeof(bool), nameof(IList.IsReadOnly), new MethodPropertyBody(Return(True), Assign(new FormattableStringToExpression($"{nameof(IList.IsReadOnly)}"), new KeywordExpression("value", null))), ExplicitInterface: typeof(IList)); - var property3 = new PropertyDeclaration($"To test an auto property with an internal setter", MethodSignatureModifiers.Public, typeof(int), nameof(IReadOnlyList.Count), new MethodPropertyBody(Return(Literal(299792458)), Assign(new FormattableStringToExpression($"{nameof(IReadOnlyList.Count)}"), new KeywordExpression("value", null)), MethodSignatureModifiers.Internal), ExplicitInterface: typeof(IReadOnlyList)); + var property1 = new PropertyProvider($"To test an auto property without a setter", MethodSignatureModifiers.Public, typeof(int), nameof(IList.Count), new MethodPropertyBody(Return(Literal(299792458))), explicitInterface: typeof(IList)); + var property2 = new PropertyProvider($"To test an auto property with a setter", MethodSignatureModifiers.Public, typeof(bool), nameof(IList.IsReadOnly), new MethodPropertyBody(Return(True), Assign(This.Property($"{nameof(IList.IsReadOnly)}"), new KeywordExpression("value", null))), explicitInterface: typeof(IList)); + var property3 = new PropertyProvider($"To test an auto property with an internal setter", MethodSignatureModifiers.Public, typeof(int), nameof(IReadOnlyList.Count), new MethodPropertyBody(Return(Literal(299792458)), Assign(This.Property($"{nameof(IReadOnlyList.Count)}"), new KeywordExpression("value", null)), MethodSignatureModifiers.Internal), explicitInterface: typeof(IReadOnlyList)); - var codeWriter = new CodeWriter(); + using var codeWriter = new CodeWriter(); codeWriter.WriteProperty(property1); - codeWriter.Append($"// test comment"); + codeWriter.WriteLine($"// test comment"); codeWriter.WriteProperty(property2); codeWriter.WriteProperty(property3); @@ -483,35 +504,35 @@ public void CodeWriter_WriteProperty_MethodPropertyBody_WithExplicitInterface() .Append("/// To test an auto property without a setter. ").Append(NewLine) .Append("public int global::System.Collections.Generic.IList.Count").Append(NewLine) .Append("{").Append(NewLine) - .Append("get").Append(NewLine) - .Append("{").Append(NewLine) - .Append("return 299792458;").Append(NewLine) - .Append("}").Append(NewLine) + .Append(" get").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" return 299792458;").Append(NewLine) + .Append(" }").Append(NewLine) .Append("}").Append(NewLine) .Append("// test comment").Append(NewLine) .Append("/// To test an auto property with a setter. ").Append(NewLine) .Append("public bool global::System.Collections.Generic.IList.IsReadOnly").Append(NewLine) .Append("{").Append(NewLine) - .Append("get").Append(NewLine) - .Append("{").Append(NewLine) - .Append("return true;").Append(NewLine) - .Append("}").Append(NewLine) - .Append("set").Append(NewLine) - .Append("{").Append(NewLine) - .Append("IsReadOnly = value;").Append(NewLine) - .Append("}").Append(NewLine) + .Append(" get").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" return true;").Append(NewLine) + .Append(" }").Append(NewLine) + .Append(" set").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" this.IsReadOnly = value;").Append(NewLine) + .Append(" }").Append(NewLine) .Append("}").Append(NewLine) .Append("/// To test an auto property with an internal setter. ").Append(NewLine) .Append("public int global::System.Collections.Generic.IReadOnlyList.Count").Append(NewLine) .Append("{").Append(NewLine) - .Append("get").Append(NewLine) - .Append("{").Append(NewLine) - .Append("return 299792458;").Append(NewLine) - .Append("}").Append(NewLine) - .Append("internal set").Append(NewLine) - .Append("{").Append(NewLine) - .Append("Count = value;").Append(NewLine) - .Append("}").Append(NewLine) + .Append(" get").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" return 299792458;").Append(NewLine) + .Append(" }").Append(NewLine) + .Append(" internal set").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" this.Count = value;").Append(NewLine) + .Append(" }").Append(NewLine) .Append("}").Append(NewLine) .ToString(); @@ -523,18 +544,18 @@ public void CodeWriter_WriteProperty_MethodPropertyBody_WithExplicitInterface() [Test] public void CodeWriter_WriteProperty_IndexerProperty_AutoBody() { - var p1 = new Parameter("p1", $"p1", typeof(int), null, ValidationType.None, null); - var indexer1 = new IndexerDeclaration($"To test an auto property without a setter", MethodSignatureModifiers.Public, typeof(float), p1, new AutoPropertyBody(false)); - var p2 = new Parameter("p2", $"p2", typeof(string), null, ValidationType.None, null); - var indexer2 = new IndexerDeclaration($"To test an auto property with a setter", MethodSignatureModifiers.Public, typeof(bool), p2, new AutoPropertyBody(true, MethodSignatureModifiers.None)); - var p3 = new Parameter("p3", $"p3", typeof(float), null, ValidationType.None, null); - var indexer3 = new IndexerDeclaration($"To test an auto property with an internal setter", MethodSignatureModifiers.Public, typeof(double), p3, new AutoPropertyBody(true, MethodSignatureModifiers.Internal)); - var p4 = new Parameter("p4", $"p4", typeof(double), null, ValidationType.None, null); - var indexer4 = new IndexerDeclaration($"To test an auto property with an internal setter and initialization value", MethodSignatureModifiers.Public, typeof(string), p4, new AutoPropertyBody(true, MethodSignatureModifiers.Internal, Literal("abc"))); - - var codeWriter = new CodeWriter(); + var p1 = new ParameterProvider("p1", $"p1", typeof(int), null); + var indexer1 = new IndexerProvider($"To test an auto property without a setter", MethodSignatureModifiers.Public, typeof(float), p1, new AutoPropertyBody(false)); + var p2 = new ParameterProvider("p2", $"p2", typeof(string), null); + var indexer2 = new IndexerProvider($"To test an auto property with a setter", MethodSignatureModifiers.Public, typeof(bool), p2, new AutoPropertyBody(true, MethodSignatureModifiers.None)); + var p3 = new ParameterProvider("p3", $"p3", typeof(float), null); + var indexer3 = new IndexerProvider($"To test an auto property with an internal setter", MethodSignatureModifiers.Public, typeof(double), p3, new AutoPropertyBody(true, MethodSignatureModifiers.Internal)); + var p4 = new ParameterProvider("p4", $"p4", typeof(double), null); + var indexer4 = new IndexerProvider($"To test an auto property with an internal setter and initialization value", MethodSignatureModifiers.Public, typeof(string), p4, new AutoPropertyBody(true, MethodSignatureModifiers.Internal, Literal("abc"))); + + using var codeWriter = new CodeWriter(); codeWriter.WriteProperty(indexer1); - codeWriter.Append($"// test comment"); + codeWriter.WriteLine($"// test comment"); codeWriter.WriteProperty(indexer2); codeWriter.WriteProperty(indexer3); codeWriter.WriteProperty(indexer4); @@ -542,14 +563,14 @@ public void CodeWriter_WriteProperty_IndexerProperty_AutoBody() var expected = new StringBuilder() .Append(_header) .Append("/// To test an auto property without a setter. ").Append(NewLine) - .Append("public float this[int p1]{ get; }").Append(NewLine) + .Append("public float this[int p1] { get; }").Append(NewLine) .Append("// test comment").Append(NewLine) .Append("/// To test an auto property with a setter. ").Append(NewLine) - .Append("public bool this[string p2]{ get;set; }").Append(NewLine) + .Append("public bool this[string p2] { get; set; }").Append(NewLine) .Append("/// To test an auto property with an internal setter. ").Append(NewLine) - .Append("public double this[float p3]{ get;internal set; }").Append(NewLine) + .Append("public double this[float p3] { get; internal set; }").Append(NewLine) .Append("/// To test an auto property with an internal setter and initialization value. ").Append(NewLine) - .Append("public string this[double p4]{ get;internal set; } = \"abc\";").Append(NewLine) + .Append("public string this[double p4] { get; internal set; } = \"abc\";").Append(NewLine) .ToString(); var result = codeWriter.ToString(); @@ -560,14 +581,14 @@ public void CodeWriter_WriteProperty_IndexerProperty_AutoBody() [Test] public void CodeWriter_WriteProperty_IndexerProperty_AutoBody_WithExplicitInterface() { - var index = new Parameter("index", $"index", typeof(int), null, ValidationType.None, null); - var indexer1 = new IndexerDeclaration($"To test an auto property without a setter", MethodSignatureModifiers.Public, typeof(string), index, new AutoPropertyBody(false), ExplicitInterface: typeof(IReadOnlyList)); - var indexer2 = new IndexerDeclaration($"To test an auto property with a setter", MethodSignatureModifiers.Public, typeof(bool), index, new AutoPropertyBody(true, MethodSignatureModifiers.None), ExplicitInterface: typeof(IList)); - var indexer3 = new IndexerDeclaration($"To test an auto property with an internal setter", MethodSignatureModifiers.Public, typeof(double), index, new AutoPropertyBody(true, MethodSignatureModifiers.Internal), ExplicitInterface: typeof(IReadOnlyList)); + var index = new ParameterProvider("index", $"index", typeof(int), null); + var indexer1 = new IndexerProvider($"To test an auto property without a setter", MethodSignatureModifiers.Public, typeof(string), index, new AutoPropertyBody(false), explicitInterface: typeof(IReadOnlyList)); + var indexer2 = new IndexerProvider($"To test an auto property with a setter", MethodSignatureModifiers.Public, typeof(bool), index, new AutoPropertyBody(true, MethodSignatureModifiers.None), explicitInterface: typeof(IList)); + var indexer3 = new IndexerProvider($"To test an auto property with an internal setter", MethodSignatureModifiers.Public, typeof(double), index, new AutoPropertyBody(true, MethodSignatureModifiers.Internal), explicitInterface: typeof(IReadOnlyList)); - var codeWriter = new CodeWriter(); + using var codeWriter = new CodeWriter(); codeWriter.WriteProperty(indexer1); - codeWriter.Append($"// test comment"); + codeWriter.WriteLine($"// test comment"); codeWriter.WriteProperty(indexer2); codeWriter.WriteProperty(indexer3); @@ -575,12 +596,12 @@ public void CodeWriter_WriteProperty_IndexerProperty_AutoBody_WithExplicitInterf .Append(_header) .Append("using System.Collections.Generic;").Append(NewLine).Append(NewLine) .Append("/// To test an auto property without a setter. ").Append(NewLine) - .Append("public string global::System.Collections.Generic.IReadOnlyList.this[int index]{ get; }").Append(NewLine) + .Append("public string global::System.Collections.Generic.IReadOnlyList.this[int index] { get; }").Append(NewLine) .Append("// test comment").Append(NewLine) .Append("/// To test an auto property with a setter. ").Append(NewLine) - .Append("public bool global::System.Collections.Generic.IList.this[int index]{ get;set; }").Append(NewLine) + .Append("public bool global::System.Collections.Generic.IList.this[int index] { get; set; }").Append(NewLine) .Append("/// To test an auto property with an internal setter. ").Append(NewLine) - .Append("public double global::System.Collections.Generic.IReadOnlyList.this[int index]{ get;internal set; }").Append(NewLine) + .Append("public double global::System.Collections.Generic.IReadOnlyList.this[int index] { get; internal set; }").Append(NewLine) .ToString(); var result = codeWriter.ToString(); @@ -591,14 +612,14 @@ public void CodeWriter_WriteProperty_IndexerProperty_AutoBody_WithExplicitInterf [Test] public void CodeWriter_WriteProperty_IndexerProperty_ExpressionBody() { - var p1 = new Parameter("p1", $"p1", typeof(int), null, ValidationType.None, null); - var indexer1 = new IndexerDeclaration($"To test an expression property with string type", MethodSignatureModifiers.Public, typeof(string), p1, new ExpressionPropertyBody(Literal("abc"))); - var p2 = new Parameter("p2", $"p2", typeof(string), null, ValidationType.None, null); - var indexer2 = new IndexerDeclaration($"To test an expression property with int type", MethodSignatureModifiers.Public, typeof(int), p2, new ExpressionPropertyBody(Literal(299792458))); + var p1 = new ParameterProvider("p1", $"p1", typeof(int), null); + var indexer1 = new IndexerProvider($"To test an expression property with string type", MethodSignatureModifiers.Public, typeof(string), p1, new ExpressionPropertyBody(Literal("abc"))); + var p2 = new ParameterProvider("p2", $"p2", typeof(string), null); + var indexer2 = new IndexerProvider($"To test an expression property with int type", MethodSignatureModifiers.Public, typeof(int), p2, new ExpressionPropertyBody(Literal(299792458))); - var codeWriter = new CodeWriter(); + using var codeWriter = new CodeWriter(); codeWriter.WriteProperty(indexer1); - codeWriter.Append($"// test comment"); + codeWriter.WriteLine($"// test comment"); codeWriter.WriteProperty(indexer2); var expected = new StringBuilder() @@ -618,14 +639,14 @@ public void CodeWriter_WriteProperty_IndexerProperty_ExpressionBody() [Test] public void CodeWriter_WriteProperty_IndexerProperty_ExpressionBody_WithExplicitInterface() { - var p1 = new Parameter("index", $"index", typeof(int), null, ValidationType.None, null); - var indexer1 = new IndexerDeclaration($"To test an expression property with string type", MethodSignatureModifiers.Public, typeof(string), p1, new ExpressionPropertyBody(Literal("abc")), ExplicitInterface: typeof(IReadOnlyList)); - var p2 = new Parameter("key", $"key", typeof(string), null, ValidationType.None, null); - var indexer2 = new IndexerDeclaration($"To test an expression property with int type", MethodSignatureModifiers.Public, typeof(int), p2, new ExpressionPropertyBody(Literal(299792458)), ExplicitInterface: typeof(IReadOnlyDictionary)); + var p1 = new ParameterProvider("index", $"index", typeof(int), null); + var indexer1 = new IndexerProvider($"To test an expression property with string type", MethodSignatureModifiers.Public, typeof(string), p1, new ExpressionPropertyBody(Literal("abc")), explicitInterface: typeof(IReadOnlyList)); + var p2 = new ParameterProvider("key", $"key", typeof(string), null); + var indexer2 = new IndexerProvider($"To test an expression property with int type", MethodSignatureModifiers.Public, typeof(int), p2, new ExpressionPropertyBody(Literal(299792458)), explicitInterface: typeof(IReadOnlyDictionary)); - var codeWriter = new CodeWriter(); + using var codeWriter = new CodeWriter(); codeWriter.WriteProperty(indexer1); - codeWriter.Append($"// test comment"); + codeWriter.WriteLine($"// test comment"); codeWriter.WriteProperty(indexer2); var expected = new StringBuilder() @@ -646,16 +667,16 @@ public void CodeWriter_WriteProperty_IndexerProperty_ExpressionBody_WithExplicit [Test] public void CodeWriter_WriteProperty_IndexerProperty_MethodPropertyBody() { - var p1 = new Parameter("p1", $"p1", typeof(int), null, ValidationType.None, null); - var indexer1 = new IndexerDeclaration($"To test a method property without a setter", MethodSignatureModifiers.Public, typeof(string), p1, new MethodPropertyBody(Return(Literal("abc")))); - var p2 = new Parameter("p2", $"p2", typeof(int), null, ValidationType.None, null); - var indexer2 = new IndexerDeclaration($"To test a method property with a setter", MethodSignatureModifiers.Public, typeof(string), p2, new MethodPropertyBody(Return(Literal("abc")), Assign(new FormattableStringToExpression($"Property2"), new KeywordExpression("value", null)))); - var p3 = new Parameter("p3", $"p3", typeof(int), null, ValidationType.None, null); - var indexer3 = new IndexerDeclaration($"To test a method property with an internal setter", MethodSignatureModifiers.Public, typeof(string), p3, new MethodPropertyBody(Return(Literal("abc")), Assign(new FormattableStringToExpression($"Property3"), new KeywordExpression("value", null)), MethodSignatureModifiers.Internal)); - - var codeWriter = new CodeWriter(); + var p1 = new ParameterProvider("p1", $"p1", typeof(int), null); + var indexer1 = new IndexerProvider($"To test a method property without a setter", MethodSignatureModifiers.Public, typeof(string), p1, new MethodPropertyBody(Return(Literal("abc")))); + var p2 = new ParameterProvider("p2", $"p2", typeof(int), null); + var indexer2 = new IndexerProvider($"To test a method property with a setter", MethodSignatureModifiers.Public, typeof(string), p2, new MethodPropertyBody(Return(Literal("abc")), Assign(This.Property($"Property2"), new KeywordExpression("value", null)))); + var p3 = new ParameterProvider("p3", $"p3", typeof(int), null); + var indexer3 = new IndexerProvider($"To test a method property with an internal setter", MethodSignatureModifiers.Public, typeof(string), p3, new MethodPropertyBody(Return(Literal("abc")), Assign(This.Property($"Property3"), new KeywordExpression("value", null)), MethodSignatureModifiers.Internal)); + + using var codeWriter = new CodeWriter(); codeWriter.WriteProperty(indexer1); - codeWriter.Append($"// test comment"); + codeWriter.WriteLine($"// test comment"); codeWriter.WriteProperty(indexer2); codeWriter.WriteProperty(indexer3); @@ -664,35 +685,35 @@ public void CodeWriter_WriteProperty_IndexerProperty_MethodPropertyBody() .Append("/// To test a method property without a setter. ").Append(NewLine) .Append("public string this[int p1]").Append(NewLine) .Append("{").Append(NewLine) - .Append("get").Append(NewLine) - .Append("{").Append(NewLine) - .Append("return \"abc\";").Append(NewLine) - .Append("}").Append(NewLine) + .Append(" get").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" return \"abc\";").Append(NewLine) + .Append(" }").Append(NewLine) .Append("}").Append(NewLine) .Append("// test comment").Append(NewLine) .Append("/// To test a method property with a setter. ").Append(NewLine) .Append("public string this[int p2]").Append(NewLine) .Append("{").Append(NewLine) - .Append("get").Append(NewLine) - .Append("{").Append(NewLine) - .Append("return \"abc\";").Append(NewLine) - .Append("}").Append(NewLine) - .Append("set").Append(NewLine) - .Append("{").Append(NewLine) - .Append("Property2 = value;").Append(NewLine) - .Append("}").Append(NewLine) + .Append(" get").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" return \"abc\";").Append(NewLine) + .Append(" }").Append(NewLine) + .Append(" set").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" this.Property2 = value;").Append(NewLine) + .Append(" }").Append(NewLine) .Append("}").Append(NewLine) .Append("/// To test a method property with an internal setter. ").Append(NewLine) .Append("public string this[int p3]").Append(NewLine) .Append("{").Append(NewLine) - .Append("get").Append(NewLine) - .Append("{").Append(NewLine) - .Append("return \"abc\";").Append(NewLine) - .Append("}").Append(NewLine) - .Append("internal set").Append(NewLine) - .Append("{").Append(NewLine) - .Append("Property3 = value;").Append(NewLine) - .Append("}").Append(NewLine) + .Append(" get").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" return \"abc\";").Append(NewLine) + .Append(" }").Append(NewLine) + .Append(" internal set").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" this.Property3 = value;").Append(NewLine) + .Append(" }").Append(NewLine) .Append("}").Append(NewLine) .ToString(); @@ -704,14 +725,14 @@ public void CodeWriter_WriteProperty_IndexerProperty_MethodPropertyBody() [Test] public void CodeWriter_WriteProperty_IndexerProperty_MethodPropertyBody_WithExplicitInterface() { - var index = new Parameter("index", $"index", typeof(int), null, ValidationType.None, null); - var indexer1 = new IndexerDeclaration($"To test a method property without a setter", MethodSignatureModifiers.Public, typeof(string), index, new MethodPropertyBody(Return(Literal("abc"))), ExplicitInterface: typeof(IReadOnlyList)); - var indexer2 = new IndexerDeclaration($"To test a method property with a setter", MethodSignatureModifiers.Public, typeof(string), index, new MethodPropertyBody(Return(Literal("abc")), Assign(new FormattableStringToExpression($"Property2"), new KeywordExpression("value", null))), ExplicitInterface: typeof(IList)); - var indexer3 = new IndexerDeclaration($"To test a method property with an internal setter", MethodSignatureModifiers.Public, typeof(string), index, new MethodPropertyBody(Return(Literal("abc")), Assign(new FormattableStringToExpression($"Property3"), new KeywordExpression("value", null)), MethodSignatureModifiers.Internal), ExplicitInterface: typeof(IReadOnlyDictionary)); + var index = new ParameterProvider("index", $"index", typeof(int), null); + var indexer1 = new IndexerProvider($"To test a method property without a setter", MethodSignatureModifiers.Public, typeof(string), index, new MethodPropertyBody(Return(Literal("abc"))), explicitInterface: typeof(IReadOnlyList)); + var indexer2 = new IndexerProvider($"To test a method property with a setter", MethodSignatureModifiers.Public, typeof(string), index, new MethodPropertyBody(Return(Literal("abc")), Assign(This.Property($"Property2"), new KeywordExpression("value", null))), explicitInterface: typeof(IList)); + var indexer3 = new IndexerProvider($"To test a method property with an internal setter", MethodSignatureModifiers.Public, typeof(string), index, new MethodPropertyBody(Return(Literal("abc")), Assign(This.Property($"Property3"), new KeywordExpression("value", null)), MethodSignatureModifiers.Internal), explicitInterface: typeof(IReadOnlyDictionary)); - var codeWriter = new CodeWriter(); + using var codeWriter = new CodeWriter(); codeWriter.WriteProperty(indexer1); - codeWriter.Append($"// test comment"); + codeWriter.WriteLine($"// test comment"); codeWriter.WriteProperty(indexer2); codeWriter.WriteProperty(indexer3); @@ -721,35 +742,35 @@ public void CodeWriter_WriteProperty_IndexerProperty_MethodPropertyBody_WithExpl .Append("/// To test a method property without a setter. ").Append(NewLine) .Append("public string global::System.Collections.Generic.IReadOnlyList.this[int index]").Append(NewLine) .Append("{").Append(NewLine) - .Append("get").Append(NewLine) - .Append("{").Append(NewLine) - .Append("return \"abc\";").Append(NewLine) - .Append("}").Append(NewLine) + .Append(" get").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" return \"abc\";").Append(NewLine) + .Append(" }").Append(NewLine) .Append("}").Append(NewLine) .Append("// test comment").Append(NewLine) .Append("/// To test a method property with a setter. ").Append(NewLine) .Append("public string global::System.Collections.Generic.IList.this[int index]").Append(NewLine) .Append("{").Append(NewLine) - .Append("get").Append(NewLine) - .Append("{").Append(NewLine) - .Append("return \"abc\";").Append(NewLine) - .Append("}").Append(NewLine) - .Append("set").Append(NewLine) - .Append("{").Append(NewLine) - .Append("Property2 = value;").Append(NewLine) - .Append("}").Append(NewLine) + .Append(" get").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" return \"abc\";").Append(NewLine) + .Append(" }").Append(NewLine) + .Append(" set").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" this.Property2 = value;").Append(NewLine) + .Append(" }").Append(NewLine) .Append("}").Append(NewLine) .Append("/// To test a method property with an internal setter. ").Append(NewLine) .Append("public string global::System.Collections.Generic.IReadOnlyDictionary.this[int index]").Append(NewLine) .Append("{").Append(NewLine) - .Append("get").Append(NewLine) - .Append("{").Append(NewLine) - .Append("return \"abc\";").Append(NewLine) - .Append("}").Append(NewLine) - .Append("internal set").Append(NewLine) - .Append("{").Append(NewLine) - .Append("Property3 = value;").Append(NewLine) - .Append("}").Append(NewLine) + .Append(" get").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" return \"abc\";").Append(NewLine) + .Append(" }").Append(NewLine) + .Append(" internal set").Append(NewLine) + .Append(" {").Append(NewLine) + .Append(" this.Property3 = value;").Append(NewLine) + .Append(" }").Append(NewLine) .Append("}").Append(NewLine) .ToString(); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ConfigurationTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ConfigurationTests.cs index 8d03a3c1e86..eb81f28ee1b 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ConfigurationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ConfigurationTests.cs @@ -4,11 +4,11 @@ using System; using System.Collections.Generic; using System.IO; -using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Snippets; using Moq; using NUnit.Framework; using NUnit.Framework.Internal; -using static Microsoft.Generator.CSharp.Expressions.ExtensibleSnippets; +using static Microsoft.Generator.CSharp.Snippets.ExtensibleSnippets; namespace Microsoft.Generator.CSharp.Tests { diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/KnownParametersTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/KnownParametersTests.cs index eae35ff5ed3..be05196fbdd 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/KnownParametersTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/KnownParametersTests.cs @@ -1,83 +1,50 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; -using Moq; +using System.IO; using NUnit.Framework; namespace Microsoft.Generator.CSharp.Tests { -#pragma warning disable CS8618 internal class KnownParametersTests { - private Mock _factory; - private KnownParameters _knownParameters; -#pragma warning restore CS8618 + private readonly string _mocksFolder = "Mocks"; - [SetUp] - public void Setup() + [OneTimeSetUp] + public void OneTimeSetup() { - _factory = new Mock(); - _knownParameters = new KnownParameters(_factory.Object); + var configFilePath = Path.Combine(AppContext.BaseDirectory, _mocksFolder); + // initialize the singleton instance of the plugin + _ = new MockCodeModelPlugin(new GeneratorContext(Configuration.Load(configFilePath))); } - [TestCase(false)] - [TestCase(true)] - public void TestTokenAuth(bool notImplemented) + + [Test] + public void TestTokenAuth() { - if (notImplemented) - { - _factory.Setup(x => x.TokenCredentialType()).Throws(); - Assert.That(() => _knownParameters.TokenAuth, Throws.Exception.TypeOf()); - } - else - { - _factory.Setup(x => x.TokenCredentialType()).Returns(new CSharpType(typeof(int))); - var result = _knownParameters.TokenAuth; - Assert.IsNotNull(result); - Assert.IsNotNull(result.Type); - Assert.IsTrue(result.Type.Equals(new CSharpType(typeof(int)))); - } + var result = KnownParameters.TokenAuth; + Assert.IsNotNull(result); + Assert.IsNotNull(result.Type); + Assert.IsTrue(result.Type.Equals(new CSharpType(typeof(int)))); } - [TestCase(false)] - [TestCase(true)] - public void TestMatchConditionsParameter(bool notImplemented) + [TestCase] + public void TestMatchConditionsParameter() { - if (notImplemented) - { - _factory.Setup(x => x.MatchConditionsType()).Throws(); - Assert.That(() => _knownParameters.MatchConditionsParameter, Throws.Exception.TypeOf()); - } - else - { - _factory.Setup(x => x.RequestConditionsType()).Returns(new CSharpType(typeof(int))); - _factory.Setup(x => x.MatchConditionsType()).Returns(new CSharpType(typeof(int))); - var result = _knownParameters.MatchConditionsParameter; - Assert.IsNotNull(result); - Assert.IsNotNull(result.Type); - Assert.IsTrue(result.Type.Equals(new CSharpType(typeof(int)))); - } + var result = KnownParameters.MatchConditionsParameter; + Assert.IsNotNull(result); + Assert.IsNotNull(result.Type); + Assert.IsTrue(result.Type.Equals(new CSharpType(typeof(int)))); } - [TestCase(false)] - [TestCase(true)] - public void TestRequestConditionsParameter(bool notImplemented) + [TestCase] + public void TestRequestConditionsParameter() { - if (notImplemented) - { - _factory.Setup(x => x.RequestConditionsType()).Throws(); - Assert.That(() => _knownParameters.RequestConditionsParameter, Throws.Exception.TypeOf()); - } - else - { - _factory.Setup(x => x.RequestConditionsType()).Returns(new CSharpType(typeof(int))); - _factory.Setup(x => x.MatchConditionsType()).Returns(new CSharpType(typeof(int))); - var result = _knownParameters.RequestConditionsParameter; - Assert.IsNotNull(result); - Assert.IsNotNull(result.Type); - Assert.IsTrue(result.Type.Equals(new CSharpType(typeof(int)))); - } + var result = KnownParameters.RequestConditionsParameter; + Assert.IsNotNull(result); + Assert.IsNotNull(result.Type); + Assert.IsTrue(result.Type.Equals(new CSharpType(typeof(int)))); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/KnownValueExpressionsTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/KnownValueExpressionsTests.cs index 491040a0b3b..d0c37cac285 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/KnownValueExpressionsTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/KnownValueExpressionsTests.cs @@ -1,10 +1,12 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; using System.Collections.Generic; using System.Linq; using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Snippets; +using Microsoft.Generator.CSharp.Statements; using NUnit.Framework; namespace Microsoft.Generator.CSharp.Tests @@ -16,11 +18,11 @@ public void BinaryOperatorExpressionWithOrOperator() { var left = new ValueExpression(); var right = new ValueExpression(); - var boolExpression = new BoolExpression(left); + var boolExpression = new BoolSnippet(left); var result = boolExpression.Or(right); - Assert.AreEqual(new BinaryOperatorExpression(" || ", boolExpression, right), result.Untyped); + Assert.AreEqual(new BinaryOperatorExpression("||", boolExpression, right), result.Untyped); } [Test] @@ -28,11 +30,11 @@ public void BinaryOperatorExpressionWithAndOperator() { var left = new ValueExpression(); var right = new ValueExpression(); - var boolExpression = new BoolExpression(left); + var boolExpression = new BoolSnippet(left); var result = boolExpression.And(right); - Assert.AreEqual(new BinaryOperatorExpression(" && ", boolExpression, right), result.Untyped); + Assert.AreEqual(new BinaryOperatorExpression("&&", boolExpression, right), result.Untyped); } [TestCase(typeof(int))] @@ -53,7 +55,7 @@ public void ListExpression(Type T) var itemType = new CSharpType(T); var untypedValue = new ValueExpression(); - var listExpression = new ListExpression(itemType, untypedValue); + var listExpression = new ListSnippet(itemType, untypedValue); Assert.AreEqual(itemType, listExpression.ItemType); Assert.AreEqual(untypedValue, listExpression.Untyped); @@ -63,7 +65,7 @@ public void ListExpression(Type T) public void ListExpressionAddItem() { var item = new ValueExpression(); - var listExpression = new ListExpression(new CSharpType(typeof(int)), new ValueExpression()); + var listExpression = new ListSnippet(new CSharpType(typeof(int)), new ValueExpression()); var result = listExpression.Add(item); @@ -93,7 +95,7 @@ public void DictionaryExpression(Type t1, Type t2) var valueType = new CSharpType(t2); var untypedValue = new ValueExpression(); - var dictionaryExpression = new DictionaryExpression(t1, t2, untypedValue); + var dictionaryExpression = new DictionarySnippet(t1, t2, untypedValue); Assert.AreEqual(keyType, dictionaryExpression.KeyType); Assert.AreEqual(valueType, dictionaryExpression.ValueType); @@ -105,7 +107,7 @@ public void DictionaryExpressionAddItems() { var keyType = new CSharpType(typeof(int)); var valueType = new CSharpType(typeof(string)); - var dictionaryExpression = new DictionaryExpression(keyType, valueType, new ValueExpression()); + var dictionaryExpression = new DictionarySnippet(keyType, valueType, new ValueExpression()); var key = new ValueExpression(); var value = new ValueExpression(); @@ -137,9 +139,9 @@ public void KeyValuePairExpression(Type t1, Type t2) var valueType = new CSharpType(t2); var untypedValue = new ValueExpression(); - var keyValuePairExpression = new KeyValuePairExpression(keyType, valueType, untypedValue); - var expectedKey = new TypedMemberExpression(keyValuePairExpression.Untyped, nameof(KeyValuePair.Key), keyType); - var expectedValue = new TypedMemberExpression(keyValuePairExpression.Untyped, nameof(KeyValuePair.Value), valueType); + var keyValuePairExpression = new KeyValuePairSnippet(keyType, valueType, untypedValue); + var expectedKey = new MemberExpression(keyValuePairExpression.Untyped, nameof(KeyValuePair.Key)); + var expectedValue = new MemberExpression(keyValuePairExpression.Untyped, nameof(KeyValuePair.Value)); Assert.AreEqual(expectedKey, keyValuePairExpression.Key); Assert.AreEqual(expectedValue, keyValuePairExpression.Value); @@ -152,11 +154,11 @@ public void EnumerableExpressionWithAnyMethodCall() { var itemType = new CSharpType(typeof(int)); var untypedValue = new ValueExpression(); - var enumerableExpression = new EnumerableExpression(itemType, untypedValue); + var enumerableExpression = new EnumerableSnippet(itemType, untypedValue); var result = enumerableExpression.Any(); - var expectedExpression = new BoolExpression( + var expectedExpression = new BoolSnippet( new InvokeStaticMethodExpression( typeof(Enumerable), nameof(Enumerable.Any), diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Microsoft.Generator.CSharp.Tests.csproj b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Microsoft.Generator.CSharp.Tests.csproj index 8aac1c11e34..48aa3d8ef1e 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Microsoft.Generator.CSharp.Tests.csproj +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Microsoft.Generator.CSharp.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -16,6 +16,7 @@ + diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/Expressions/MockExpression.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/Expressions/MockExpression.cs index 396c7dde9f1..b4d9755b354 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/Expressions/MockExpression.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/Expressions/MockExpression.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using Microsoft.Generator.CSharp.Expressions; @@ -7,7 +7,7 @@ namespace Microsoft.Generator.CSharp.Tests { internal record MockExpression : ValueExpression { - public override void Write(CodeWriter writer) + internal override void Write(CodeWriter writer) { writer.AppendRaw("Custom implementation"); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/MockCodeModelPlugin.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/MockCodeModelPlugin.cs index 6c84c9c9306..7a570855d22 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/MockCodeModelPlugin.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/MockCodeModelPlugin.cs @@ -2,7 +2,10 @@ // Licensed under the MIT License. using System; -using Microsoft.Generator.CSharp.Expressions; +using System.Collections.Generic; +using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Snippets; namespace Microsoft.Generator.CSharp.Tests { @@ -18,10 +21,10 @@ public MockCodeModelPlugin(GeneratorContext context) : base(context) } public override ApiTypes ApiTypes => throw new NotImplementedException(); - public override CodeWriterExtensionMethods CodeWriterExtensionMethods => new CustomCodeWriterExtensionMethods(); - public override TypeFactory TypeFactory => throw new NotImplementedException(); + public override TypeFactory TypeFactory => new MockTypeFactory(); public override ExtensibleSnippets ExtensibleSnippets => throw new NotImplementedException(); public override OutputLibrary OutputLibrary => throw new NotImplementedException(); - public override TypeProvider[] GetSerializationTypeProviders(ModelTypeProvider provider) => throw new NotImplementedException(); + public override IReadOnlyList GetSerializationTypeProviders(ModelProvider provider, InputModelType inputModel) => throw new NotImplementedException(); + public override string LicenseString => "// License string"; } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/MockCodeWriterDeclaration.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/MockCodeWriterDeclaration.cs new file mode 100644 index 00000000000..4eb5e27ba2b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/MockCodeWriterDeclaration.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Generator.CSharp.Tests +{ + /// + /// Can be used to create a CodeWriterDeclaration with a requested name and an optional actual name. + /// This allows you to unit test parts of the writer that assume the declaration was done + /// without actually doing the declaration + /// + internal class MockCodeWriterDeclaration + { + public string RequestedName { get; } + private string? ActualName { get; } + + public MockCodeWriterDeclaration(string requestedName, string? actualName = null) + { + RequestedName = requestedName; + ActualName = actualName; + } + + public static implicit operator CodeWriterDeclaration(MockCodeWriterDeclaration mockCodeWriterDeclaration) + { + var result = new CodeWriterDeclaration(mockCodeWriterDeclaration.RequestedName); + result.SetActualName(mockCodeWriterDeclaration.ActualName ?? mockCodeWriterDeclaration.RequestedName); + return result; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/MockTypeFactory.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/MockTypeFactory.cs new file mode 100644 index 00000000000..fe74d8e11b4 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/MockTypeFactory.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Providers; + +namespace Microsoft.Generator.CSharp.Tests +{ + internal class MockTypeFactory : TypeFactory + { + public override CSharpMethodCollection? CreateCSharpMethodCollection(InputOperation operation) + { + throw new NotImplementedException(); + } + + public override ParameterProvider CreateCSharpParam(InputParameter parameter) + { + throw new NotImplementedException(); + } + + public override CSharpType CreateCSharpType(InputType input) + { + throw new NotImplementedException(); + } + + public override CSharpType MatchConditionsType() => typeof(int); + + public override CSharpType PageResponseType() => typeof(int); + + public override CSharpType RequestConditionsType() => typeof(int); + + public override CSharpType TokenCredentialType() => typeof(int); + public override CSharpType ListInitializationType => new CSharpType(typeof(List<>), arguments: typeof(int)); + public override CSharpType DictionaryInitializationType => new CSharpType(typeof(Dictionary<,>), arguments: [typeof(string), typeof(int)]); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/Writers/CustomCodeWriterExtensionMethods.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/Writers/CustomCodeWriterExtensionMethods.cs deleted file mode 100644 index b948275e874..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Mocks/Writers/CustomCodeWriterExtensionMethods.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Generator.CSharp.Tests -{ - internal class CustomCodeWriterExtensionMethods : CodeWriterExtensionMethods - { - public override string LicenseString => "// License string"; - - public override void WriteMethod(CodeWriter writer, CSharpMethod method) - { - writer.AppendRaw("Custom implementation"); - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Models/PropertyDescriptionTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Models/PropertyDescriptionTests.cs index 1e2340c31b0..b4a18a62312 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Models/PropertyDescriptionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Models/PropertyDescriptionTests.cs @@ -15,8 +15,8 @@ internal class PropertyDescriptionTests [TestCaseSource(nameof(BuildPropertyDescriptionTestCases))] public void BuildPropertyDescription(InputModelProperty inputModelProperty, CSharpType type) { - var propertyDescription = PropertyDescriptionBuilder.BuildPropertyDescription(inputModelProperty, type, SerializationFormat.Default, false); - var propertyDescriptionString = propertyDescription.ToString(); + IReadOnlyList propertyDescription = PropertyDescriptionBuilder.BuildPropertyDescription(inputModelProperty, type, SerializationFormat.Default, false); + var propertyDescriptionString = string.Join(Environment.NewLine, propertyDescription); Assert.IsNotNull(propertyDescription); Assert.IsNotEmpty(propertyDescriptionString); @@ -47,8 +47,8 @@ public void TestGetUnionTypesDescriptions() Assert.AreEqual(7, descriptions.Count); - var codeWriter = new CodeWriter(); - codeWriter.AppendXmlDocumentation($"", $"", descriptions.ToList().Join("\n")); + using var codeWriter = new CodeWriter(); + codeWriter.AppendXmlDocumentation($"", $"", descriptions); var actual = codeWriter.ToString(false); var expected = string.Join("\n", @@ -71,19 +71,19 @@ public static IEnumerable BuildPropertyDescriptionTestCases { // list property yield return new TestCaseData( - new InputModelProperty("prop1", "prop1", "public", new InputList("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false, false), false, false, false), + new InputModelProperty("prop1", "prop1", "public", new InputListType("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false, false), false, false, false), new CSharpType(typeof(IList))); // list of binary data property yield return new TestCaseData( - new InputModelProperty("prop1", "prop1", "public", new InputList("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.BinaryData, false), false, false), false, true, false), + new InputModelProperty("prop1", "prop1", "public", new InputListType("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.Any, false), false, false), false, true, false), new CSharpType(typeof(IReadOnlyList))); // dictionary property with binary data value yield return new TestCaseData( - new InputModelProperty("prop1", "prop1", "public", new InputDictionary("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), new InputPrimitiveType(InputPrimitiveTypeKind.BinaryData, false), false), false, false, false), + new InputModelProperty("prop1", "prop1", "public", new InputDictionaryType("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), new InputPrimitiveType(InputPrimitiveTypeKind.Any, false), false), false, false, false), new CSharpType(typeof(IDictionary))); // nullable dictionary property yield return new TestCaseData( - new InputModelProperty("prop1", "prop1", "public", new InputDictionary("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false), false, false, false), + new InputModelProperty("prop1", "prop1", "public", new InputDictionaryType("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false), false, false, false), new CSharpType(typeof(IDictionary), true)); // primitive type property yield return new TestCaseData( @@ -91,7 +91,7 @@ public static IEnumerable BuildPropertyDescriptionTestCases new CSharpType(typeof(string))); // binary data property yield return new TestCaseData( - new InputModelProperty("prop1", "prop1", "public", new InputPrimitiveType(InputPrimitiveTypeKind.BinaryData, false), false, true, false), + new InputModelProperty("prop1", "prop1", "public", new InputPrimitiveType(InputPrimitiveTypeKind.Any, false), false, true, false), new CSharpType(typeof(BinaryData))); } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Models/Types/EnumTypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Models/Types/EnumTypeProviderTests.cs new file mode 100644 index 00000000000..ebc6af51755 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Models/Types/EnumTypeProviderTests.cs @@ -0,0 +1,353 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Reflection; +using System.Text; +using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Snippets; +using Moq; +using NUnit.Framework; + +namespace Microsoft.Generator.CSharp.Tests +{ + public class EnumTypeProviderTests + { + internal const string NewLine = "\n"; + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + private GeneratorContext _generatorContext; +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + private readonly string _configFilePath = Path.Combine(AppContext.BaseDirectory, "Mocks"); + private FieldInfo? _mockPlugin; + + [SetUp] + public void Setup() + { + // initialize the mock singleton instance of the plugin + _mockPlugin = typeof(CodeModelPlugin).GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic); + _generatorContext = new GeneratorContext(Configuration.Load(_configFilePath)); + } + + [TearDown] + public void Teardown() + { + _mockPlugin?.SetValue(null, null); + } + + // Validates the int based fixed enum + [TestCase] + public void BuildEnumType_ValidateIntBasedFixedEnum() + { + var mockPluginInstance = new Mock(_generatorContext); + var mockTypeFactory = new Mock(); + mockTypeFactory.Setup(t => t.CreateCSharpType(It.IsAny())).Returns(typeof(int)); + mockPluginInstance.SetupGet(p => p.TypeFactory).Returns(mockTypeFactory.Object); + _mockPlugin?.SetValue(null, mockPluginInstance.Object); + + var input = new InputEnumType("mockInputEnum", "mockNamespace", "public", null, "The mock enum", InputModelTypeUsage.RoundTrip, new InputPrimitiveType(InputPrimitiveTypeKind.Int32), [new InputEnumTypeValue("One", 1, null), new InputEnumTypeValue("Two", 2, null)], false, false); + var enumType = EnumProvider.Create(input); + var fields = enumType.Fields; + + Assert.AreEqual(2, fields.Count); + Assert.AreEqual("One", fields[0].Name); + Assert.AreEqual("Two", fields[1].Name); + var value1 = fields[0].InitializationValue as LiteralExpression; + Assert.IsNotNull(value1); + Assert.AreEqual(1, value1?.Literal); + var value2 = fields[1].InitializationValue as LiteralExpression; + Assert.IsNotNull(value2); + Assert.AreEqual(2, value2?.Literal); + + // int based fixed enum does not have serialization method therefore we only have one method + var serialization = enumType.Serialization; + Assert.IsNotNull(serialization); + Assert.AreEqual(1, serialization?.Methods.Count); + + // validate the expression is working fine + using var writer = new CodeWriter(); + var enumVar = new VariableReferenceSnippet(enumType.Type, new MockCodeWriterDeclaration("e")); + enumType.ToSerial(enumVar).Write(writer); + writer.WriteLine(); + enumType.ToEnum(Snippet.Literal(1)).Write(writer); + + var result = writer.ToString(false); + var builder = new StringBuilder(); + builder.Append($"((int)e)").Append(NewLine) + .Append($"1.ToMockInputEnum()"); + var expected = builder.ToString(); + + Assert.AreEqual(expected, result); + } + + // Validates the float based fixed enum + [TestCase] + public void BuildEnumType_ValidateFloatBasedFixedEnum() + { + var mockPluginInstance = new Mock(_generatorContext); + var mockTypeFactory = new Mock() { }; + mockTypeFactory.Setup(t => t.CreateCSharpType(It.IsAny())).Returns(typeof(float)); + mockPluginInstance.SetupGet(p => p.TypeFactory).Returns(mockTypeFactory.Object); + _mockPlugin?.SetValue(null, mockPluginInstance.Object); + + var input = new InputEnumType("mockInputEnum", "mockNamespace", "public", null, "The mock enum", InputModelTypeUsage.RoundTrip, new InputPrimitiveType(InputPrimitiveTypeKind.Float32), [new InputEnumTypeValue("One", 1f, null), new InputEnumTypeValue("Two", 2f, null)], false, false); + var enumType = EnumProvider.Create(input); + var fields = enumType.Fields; + + Assert.AreEqual(2, fields.Count); + Assert.AreEqual("One", fields[0].Name); + Assert.AreEqual("Two", fields[1].Name); + // non-int based enum does not initialization values. + Assert.IsNull(fields[0].InitializationValue); + Assert.IsNull(fields[1].InitializationValue); + + // int float fixed enum has serialization method and deserialization method therefore we only have two methods + var serialization = enumType.Serialization; + Assert.IsNotNull(serialization); + Assert.AreEqual(2, serialization?.Methods.Count); + + // validate the expression is working fine + using var writer = new CodeWriter(); + var enumVar = new VariableReferenceSnippet(enumType.Type, new MockCodeWriterDeclaration("e")); + enumType.ToSerial(enumVar).Write(writer); + writer.WriteLine(); + enumType.ToEnum(Snippet.Literal(1f)).Write(writer); + + var result = writer.ToString(false); + var builder = new StringBuilder(); + builder.Append($"e.ToSerialSingle()").Append(NewLine) + .Append($"1F.ToMockInputEnum()"); + var expected = builder.ToString(); + + Assert.AreEqual(expected, result); + } + + // Validates the string based fixed enum + [TestCase] + public void BuildEnumType_ValidateStringBasedFixedEnum() + { + var mockPluginInstance = new Mock(_generatorContext); + var mockTypeFactory = new Mock(); + mockTypeFactory.Setup(t => t.CreateCSharpType(It.IsAny())).Returns(typeof(string)); + mockPluginInstance.SetupGet(p => p.TypeFactory).Returns(mockTypeFactory.Object); + _mockPlugin?.SetValue(null, mockPluginInstance.Object); + + var input = new InputEnumType("mockInputEnum", "mockNamespace", "public", null, "The mock enum", InputModelTypeUsage.RoundTrip, new InputPrimitiveType(InputPrimitiveTypeKind.String), [new InputEnumTypeValue("One", "1", null), new InputEnumTypeValue("Two", "2", null)], false, false); + var enumType = EnumProvider.Create(input); + var fields = enumType.Fields; + + Assert.AreEqual(2, fields.Count); + Assert.AreEqual("One", fields[0].Name); + Assert.AreEqual("Two", fields[1].Name); + // non-int based enum does not initialization values. + Assert.IsNull(fields[0].InitializationValue); + Assert.IsNull(fields[1].InitializationValue); + + // int float fixed enum has serialization method and deserialization method therefore we only have two methods + var serialization = enumType.Serialization; + Assert.IsNotNull(serialization); + Assert.AreEqual(2, serialization?.Methods.Count); + + // validate the expression is working fine + using var writer = new CodeWriter(); + var enumVar = new VariableReferenceSnippet(enumType.Type, new MockCodeWriterDeclaration("e")); + enumType.ToSerial(enumVar).Write(writer); + writer.WriteLine(); + enumType.ToEnum(Snippet.Literal("1")).Write(writer); + + var result = writer.ToString(false); + var builder = new StringBuilder(); + builder.Append($"e.ToSerialString()").Append(NewLine) + .Append($"\"1\".ToMockInputEnum()"); + var expected = builder.ToString(); + + Assert.AreEqual(expected, result); + } + + // Validates the int based extensible enum + [TestCase] + public void BuildEnumType_ValidateIntBasedExtensibleEnum() + { + var mockPluginInstance = new Mock(_generatorContext); + var mockTypeFactory = new Mock(); + mockTypeFactory.Setup(t => t.CreateCSharpType(It.IsAny())).Returns(typeof(int)); + mockPluginInstance.SetupGet(p => p.TypeFactory).Returns(mockTypeFactory.Object); + _mockPlugin?.SetValue(null, mockPluginInstance.Object); + + var input = new InputEnumType("mockInputEnum", "mockNamespace", "public", null, "The mock enum", InputModelTypeUsage.RoundTrip, new InputPrimitiveType(InputPrimitiveTypeKind.Int32), [new InputEnumTypeValue("One", 1, null), new InputEnumTypeValue("Two", 2, null)], true, false); + var enumType = EnumProvider.Create(input); + var fields = enumType.Fields; + var properties = enumType.Properties; + + // a private field + two values + Assert.AreEqual(3, fields.Count); + Assert.AreEqual("_value", fields[0].Name); + Assert.AreEqual("OneValue", fields[1].Name); + Assert.AreEqual("TwoValue", fields[2].Name); + Assert.IsNull(fields[0].InitializationValue); + var value1 = fields[1].InitializationValue as LiteralExpression; + Assert.IsNotNull(value1); + Assert.AreEqual(1, value1?.Literal); + var value2 = fields[2].InitializationValue as LiteralExpression; + Assert.IsNotNull(value2); + Assert.AreEqual(2, value2?.Literal); + + // two properties + Assert.AreEqual(2, properties.Count); + Assert.AreEqual("One", properties[0].Name); + Assert.AreEqual(MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, properties[0].Modifiers); + Assert.IsInstanceOf(properties[0].Body); + var propertyValue1 = (properties[0].Body as AutoPropertyBody)?.InitializationExpression; + Assert.IsNotNull(propertyValue1); + Assert.AreEqual("Two", properties[1].Name); + Assert.AreEqual(MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, properties[1].Modifiers); + Assert.IsInstanceOf(properties[1].Body); + var propertyValue2 = (properties[1].Body as AutoPropertyBody)?.InitializationExpression; + Assert.IsNotNull(propertyValue2); + + // extensible enums do not have serialization + var serialization = enumType.Serialization; + Assert.IsNull(serialization); + + // validate the expression is working fine + using var writer = new CodeWriter(); + var enumVar = new VariableReferenceSnippet(enumType.Type, new MockCodeWriterDeclaration("e")); + enumType.ToSerial(enumVar).Write(writer); + writer.WriteLine(); + enumType.ToEnum(Snippet.Literal(1)).Write(writer); + + var result = writer.ToString(false); + var builder = new StringBuilder(); + builder.Append($"e.ToSerialInt32()").Append(NewLine) + .Append($"new global::sample.namespace.Models.MockInputEnum(1)"); + var expected = builder.ToString(); + + Assert.AreEqual(expected, result); + } + + // Validates the float based extensible enum + [TestCase] + public void BuildEnumType_ValidateFloatBasedExtensibleEnum() + { + var mockPluginInstance = new Mock(_generatorContext); + var mockTypeFactory = new Mock(); + mockTypeFactory.Setup(t => t.CreateCSharpType(It.IsAny())).Returns(typeof(float)); + mockPluginInstance.SetupGet(p => p.TypeFactory).Returns(mockTypeFactory.Object); + _mockPlugin?.SetValue(null, mockPluginInstance.Object); + + var input = new InputEnumType("mockInputEnum", "mockNamespace", "public", null, "The mock enum", InputModelTypeUsage.RoundTrip, new InputPrimitiveType(InputPrimitiveTypeKind.Float32), [new InputEnumTypeValue("One", 1f, null), new InputEnumTypeValue("Two", 2f, null)], true, false); + var enumType = EnumProvider.Create(input); + var fields = enumType.Fields; + var properties = enumType.Properties; + + // a private field + two values + Assert.AreEqual(3, fields.Count); + Assert.AreEqual("_value", fields[0].Name); + Assert.AreEqual("OneValue", fields[1].Name); + Assert.AreEqual("TwoValue", fields[2].Name); + Assert.IsNull(fields[0].InitializationValue); + var value1 = fields[1].InitializationValue as LiteralExpression; + Assert.IsNotNull(value1); + Assert.AreEqual(1f, value1?.Literal); + var value2 = fields[2].InitializationValue as LiteralExpression; + Assert.IsNotNull(value2); + Assert.AreEqual(2f, value2?.Literal); + + // two properties + Assert.AreEqual(2, properties.Count); + Assert.AreEqual("One", properties[0].Name); + Assert.AreEqual(MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, properties[0].Modifiers); + Assert.IsInstanceOf(properties[0].Body); + var propertyValue1 = (properties[0].Body as AutoPropertyBody)?.InitializationExpression; + Assert.IsNotNull(propertyValue1); + Assert.AreEqual("Two", properties[1].Name); + Assert.AreEqual(MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, properties[1].Modifiers); + Assert.IsInstanceOf(properties[1].Body); + var propertyValue2 = (properties[1].Body as AutoPropertyBody)?.InitializationExpression; + Assert.IsNotNull(propertyValue2); + + // extensible enums do not have serialization + var serialization = enumType.Serialization; + Assert.IsNull(serialization); + + // validate the expression is working fine + using var writer = new CodeWriter(); + var enumVar = new VariableReferenceSnippet(enumType.Type, new MockCodeWriterDeclaration("e")); + enumType.ToSerial(enumVar).Write(writer); + writer.WriteLine(); + enumType.ToEnum(Snippet.Literal(1f)).Write(writer); + + var result = writer.ToString(false); + var builder = new StringBuilder(); + builder.Append($"e.ToSerialSingle()").Append(NewLine) + .Append($"new global::sample.namespace.Models.MockInputEnum(1F)"); + var expected = builder.ToString(); + + Assert.AreEqual(expected, result); + } + + // Validates the string based extensible enum + [TestCase] + public void BuildEnumType_ValidateStringBasedExtensibleEnum() + { + var mockPluginInstance = new Mock(_generatorContext); + var mockTypeFactory = new Mock(); + mockTypeFactory.Setup(t => t.CreateCSharpType(It.IsAny())).Returns(typeof(string)); + mockPluginInstance.SetupGet(p => p.TypeFactory).Returns(mockTypeFactory.Object); + _mockPlugin?.SetValue(null, mockPluginInstance.Object); + + var input = new InputEnumType("mockInputEnum", "mockNamespace", "public", null, "The mock enum", InputModelTypeUsage.RoundTrip, new InputPrimitiveType(InputPrimitiveTypeKind.String), [new InputEnumTypeValue("One", "1", null), new InputEnumTypeValue("Two", "2", null)], true, false); + var enumType = EnumProvider.Create(input); + var fields = enumType.Fields; + var properties = enumType.Properties; + + // a private field + two values + Assert.AreEqual(3, fields.Count); + Assert.AreEqual("_value", fields[0].Name); + Assert.AreEqual("OneValue", fields[1].Name); + Assert.AreEqual("TwoValue", fields[2].Name); + Assert.IsNull(fields[0].InitializationValue); + var value1 = fields[1].InitializationValue as LiteralExpression; + Assert.IsNotNull(value1); + Assert.AreEqual("1", value1?.Literal); + var value2 = fields[2].InitializationValue as LiteralExpression; + Assert.IsNotNull(value2); + Assert.AreEqual("2", value2?.Literal); + + // two properties + Assert.AreEqual(2, properties.Count); + Assert.AreEqual("One", properties[0].Name); + Assert.AreEqual(MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, properties[0].Modifiers); + Assert.IsInstanceOf(properties[0].Body); + var propertyValue1 = (properties[0].Body as AutoPropertyBody)?.InitializationExpression; + Assert.IsNotNull(propertyValue1); + Assert.AreEqual("Two", properties[1].Name); + Assert.AreEqual(MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, properties[1].Modifiers); + Assert.IsInstanceOf(properties[1].Body); + var propertyValue2 = (properties[1].Body as AutoPropertyBody)?.InitializationExpression; + Assert.IsNotNull(propertyValue2); + + // extensible enums do not have serialization + var serialization = enumType.Serialization; + Assert.IsNull(serialization); + + // validate the expression is working fine + using var writer = new CodeWriter(); + var enumVar = new VariableReferenceSnippet(enumType.Type, new MockCodeWriterDeclaration("e")); + enumType.ToSerial(enumVar).Write(writer); + writer.WriteLine(); + enumType.ToEnum(Snippet.Literal("1")).Write(writer); + + var result = writer.ToString(false); + var builder = new StringBuilder(); + builder.Append($"e.ToString()").Append(NewLine) + .Append($"new global::sample.namespace.Models.MockInputEnum(\"1\")"); + var expected = builder.ToString(); + + Assert.AreEqual(expected, result); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Models/Types/ModelTypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Models/Types/ModelTypeProviderTests.cs index 647c5cfe930..1c7082f464b 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Models/Types/ModelTypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Models/Types/ModelTypeProviderTests.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Reflection; using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Providers; using Moq; using NUnit.Framework; @@ -50,7 +51,7 @@ public void BuildProperties_ValidatePropertySetters(InputModelProperty inputMode }; var inputModel = new InputModelType("mockInputModel", "mockNamespace", "public", null, null, InputModelTypeUsage.RoundTrip, props, null, new List(), null, null, null, false); - var modelTypeProvider = new ModelTypeProvider(inputModel, null); + var modelTypeProvider = new ModelProvider(inputModel); var properties = modelTypeProvider.Properties; Assert.IsNotNull(properties); @@ -71,27 +72,27 @@ public static IEnumerable BuildProperties_ValidatePropertySettersT { // list property yield return new TestCaseData( - new InputModelProperty("prop1", "prop1", "public", new InputList("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false, false), false, false, false), + new InputModelProperty("prop1", "prop1", "public", new InputListType("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false, false), false, false, false), new CSharpType(typeof(IList)), false); // read only list property yield return new TestCaseData( - new InputModelProperty("prop1", "prop1", "public", new InputList("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false, false), false, true, false), + new InputModelProperty("prop1", "prop1", "public", new InputListType("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false, false), false, true, false), new CSharpType(typeof(IReadOnlyList)), false); // nullable list property yield return new TestCaseData( - new InputModelProperty("prop1", "prop1", "public", new InputList("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false, false), false, false, false), + new InputModelProperty("prop1", "prop1", "public", new InputListType("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false, false), false, false, false), new CSharpType(typeof(IList), true), true); // dictionary property yield return new TestCaseData( - new InputModelProperty("prop1", "prop1", "public", new InputDictionary("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false), false, false, false), + new InputModelProperty("prop1", "prop1", "public", new InputDictionaryType("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false), false, false, false), new CSharpType(typeof(IDictionary)), false); // nullable dictionary property yield return new TestCaseData( - new InputModelProperty("prop1", "prop1", "public", new InputDictionary("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false), false, false, false), + new InputModelProperty("prop1", "prop1", "public", new InputDictionaryType("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false), false, false, false), new CSharpType(typeof(IDictionary), true), true); // primitive type property @@ -106,38 +107,25 @@ public static IEnumerable BuildProperties_ValidatePropertySettersT false); // readonlymemory property yield return new TestCaseData( - new InputModelProperty("prop1", "prop1", "public", new InputList("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), true, false), false, false, false), + new InputModelProperty("prop1", "prop1", "public", new InputListType("mockProp", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), true, false), false, false, false), new CSharpType(typeof(ReadOnlyMemory<>)), true); } } - private CSharpType GetCSharpType(InputType type) + private CSharpType GetCSharpType(InputType type) => type switch { - switch (type) + InputPrimitiveType primitiveType => primitiveType.Kind switch { - case InputPrimitiveType: - { - var primitiveType = (InputPrimitiveType)type; - - if (primitiveType.Kind is InputPrimitiveTypeKind.String) - return new CSharpType(typeof(string)); - - if (primitiveType.Kind is InputPrimitiveTypeKind.Int32) - return new CSharpType(typeof(int)); - - throw new ArgumentException("Unsupported input type."); - } - case InputList: - return new CSharpType(typeof(IList)); - case InputDictionary: - return new CSharpType(typeof(IDictionary)); - case InputIntrinsicType: - return new CSharpType(typeof(BinaryData)); - default: - throw new ArgumentException("Unsupported input type."); - } - } + InputPrimitiveTypeKind.String => typeof(string), + InputPrimitiveTypeKind.Int32 => typeof(int), + InputPrimitiveTypeKind.Any => typeof(BinaryData), + _ => throw new ArgumentException("Unsupported input type.") + }, + InputListType => typeof(IList), + InputDictionaryType => typeof(IDictionary), + _ => throw new ArgumentException("Unsupported input type.") + }; [Test] public void BuildConstructor_ValidateConstructors() @@ -148,9 +136,9 @@ public void BuildConstructor_ValidateConstructors() var properties = new List{ new InputModelProperty("requiredString", "requiredString", "", InputPrimitiveType.String, true, false, false), new InputModelProperty("OptionalInt", "optionalInt", "", InputPrimitiveType.Int32, false, false, false), - new InputModelProperty("requiredCollection", "requiredCollection", "", new InputList("List", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false, false), true, false, false), - new InputModelProperty("requiredDictionary", "requiredDictionary", "", new InputDictionary("Dictionary", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false), true, false, false), - new InputModelProperty("optionalUnknown", "optional unknown", "", new InputIntrinsicType(InputIntrinsicTypeKind.Unknown), false, false, false), + new InputModelProperty("requiredCollection", "requiredCollection", "", new InputListType("List", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false, false), true, false, false), + new InputModelProperty("requiredDictionary", "requiredDictionary", "", new InputDictionaryType("Dictionary", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), new InputPrimitiveType(InputPrimitiveTypeKind.String, false), false), true, false, false), + new InputModelProperty("optionalUnknown", "optional unknown", "", InputPrimitiveType.Any, false, false, false), }; mockTypeFactory.Setup(t => t.CreateCSharpType(It.IsAny())).Returns((InputType inputType) => @@ -172,23 +160,15 @@ public void BuildConstructor_ValidateConstructors() var inputModel = new InputModelType("TestModel", "TestModel", "public", null, "Test model.", InputModelTypeUsage.RoundTrip, properties, null, Array.Empty(), null, null, null, false); - var modelTypeProvider = new ModelTypeProvider(inputModel, null); + var modelTypeProvider = new ModelProvider(inputModel); var ctors = modelTypeProvider.Constructors; Assert.IsNotNull(ctors); - Assert.AreEqual(3, ctors.Count); + Assert.AreEqual(1, ctors.Count); var initializationCtor = ctors[0]; Assert.AreEqual(MethodSignatureModifiers.Public, initializationCtor.Signature.Modifiers); Assert.AreEqual(3, initializationCtor.Signature.Parameters.Count); - - var serializationCtor = ctors[1]; - Assert.AreEqual(MethodSignatureModifiers.Internal, serializationCtor.Signature.Modifiers); - Assert.AreEqual(5, serializationCtor.Signature.Parameters.Count); - - var emptyCtor = ctors[2]; - Assert.AreEqual(MethodSignatureModifiers.Internal, emptyCtor.Signature.Modifiers); - Assert.AreEqual(0, emptyCtor.Signature.Parameters.Count); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/OptionalTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/OptionalTests.cs new file mode 100644 index 00000000000..3ee8df04cf4 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/OptionalTests.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using NUnit.Framework; +using UnbrandedTypeSpec; +using System.Collections.Generic; +using System.Text.Json; + +namespace Microsoft.Generator.CSharp.Tests +{ + public class OptionalTests + { + [Test] + public void List_Undefined_ReturnsFalse() + { + var list = new ChangeTrackingList(); + Assert.IsFalse(Optional.IsCollectionDefined(list)); + } + + [Test] + public void List_Defined_ReturnsTrue() + { + IList innerList = new List { 1, 2, 3 }; + var list = new ChangeTrackingList(innerList); + Assert.IsTrue(Optional.IsCollectionDefined(list)); + } + + [Test] + public void Dict_Undefined_ReturnsFalse() + { + var dict = new ChangeTrackingDictionary(); + Assert.IsFalse(Optional.IsCollectionDefined((IDictionary)dict)); + } + + [Test] + public void Dict_Defined_ReturnsTrue() + { + IDictionary innerDict = new Dictionary { { 1, "one" }, { 2, "two" } }; + var dict = new ChangeTrackingDictionary(innerDict); + Assert.IsTrue(Optional.IsCollectionDefined((IDictionary)dict)); + } + + [Test] + public void ReadOnlyDict_Undefined_ReturnsFalse() + { + var dict = new ChangeTrackingDictionary(); + IReadOnlyDictionary readOnlyDict = dict; + Assert.IsFalse(Optional.IsCollectionDefined(readOnlyDict)); + } + + [Test] + public void ReadOnlyDict_Defined_ReturnsTrue() + { + IReadOnlyDictionary innerDict = new Dictionary { { 1, "one" }, { 2, "two" } }; + var dict = new ChangeTrackingDictionary(innerDict); + IReadOnlyDictionary readOnlyDict = dict; + Assert.IsTrue(Optional.IsCollectionDefined(readOnlyDict)); + } + + [Test] + public void Nullable_HasValue_ReturnsTrue() + { + int? value = 5; + Assert.IsTrue(Optional.IsDefined(value)); + } + + [Test] + public void Nullable_NoValue_ReturnsFalse() + { + int? value = null; + Assert.IsFalse(Optional.IsDefined(value)); + } + + [Test] + public void Obj_NotNull_ReturnsTrue() + { + var value = new object(); + Assert.IsTrue(Optional.IsDefined(value)); + } + + + [Test] + public void Json_Defined_ReturnsTrue() + { + var value = JsonDocument.Parse("{}").RootElement; + Assert.IsTrue(Optional.IsDefined(value)); + } + + [Test] + public void Json_Undefined_ReturnsFalse() + { + var value = new JsonElement(); + Assert.IsFalse(Optional.IsDefined(value)); + } + + [Test] + public void Str_NotNull_ReturnsTrue() + { + string value = "test"; + Assert.IsTrue(Optional.IsDefined(value)); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/OutputLibraryTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/OutputLibraryTests.cs index 3797daaaa40..695211788ad 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/OutputLibraryTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/OutputLibraryTests.cs @@ -4,6 +4,7 @@ using System; using NUnit.Framework; using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Providers; namespace Microsoft.Generator.CSharp.Tests { @@ -37,12 +38,12 @@ internal class MockOutputLibrary : OutputLibrary { public MockOutputLibrary() : base() { } - public override ModelTypeProvider[] BuildModels() + public override ModelProvider[] BuildModels() { throw new NotImplementedException(); } - public override ClientTypeProvider[] BuildClients() + public override ClientProvider[] BuildClients() { throw new NotImplementedException(); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ParameterProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ParameterProviderTests.cs new file mode 100644 index 00000000000..cd94342cdaa --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ParameterProviderTests.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using Microsoft.Generator.CSharp.Providers; +using NUnit.Framework; + +namespace Microsoft.Generator.CSharp.Tests.Providers +{ + public class ParameterProviderTests + { + [Test] + public void Equals_SameInstance_ReturnsTrue() + { + // Arrange + var parameter = new ParameterProvider("name", $"Description", new CSharpType(typeof(string))); + + // Act + var result = parameter.Equals(parameter); + + // Assert + Assert.True(result); + } + + [TestCaseSource(nameof(NotEqualsTestCases))] + public void Equals(ParameterProvider p1, ParameterProvider? p2, bool areEqual) + { + var result = p1.Equals(p2); + Assert.AreEqual(areEqual, result); + } + + private static IEnumerable NotEqualsTestCases() + { + yield return new TestCaseData( + new ParameterProvider("name", $"Description", new CSharpType(typeof(string))), + null, + false); + yield return new TestCaseData( + new ParameterProvider("name", $"Description", new CSharpType(typeof(string))), + new ParameterProvider("name", $"Description", new CSharpType(typeof(int))), + false); + yield return new TestCaseData( + new ParameterProvider("name", $"Description", new CSharpType(typeof(string))), + new ParameterProvider("name", $"Description", new CSharpType(typeof(string))), + true); + yield return new TestCaseData( + new ParameterProvider("name", $"Description", new CSharpType(typeof(string))), + new ParameterProvider("name1", $"Description", new CSharpType(typeof(string))), + false); + yield return new TestCaseData( + new ParameterProvider("name", $"Description", new CSharpType(typeof(string))) + { + Attributes = [new(new CSharpType(typeof(int)), [])] + }, + new ParameterProvider("name1", $"Description", new CSharpType(typeof(string))) + { + Attributes = [new(new CSharpType(typeof(string)), [])] + }, + false); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/SnippetTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/SnippetTests.cs new file mode 100644 index 00000000000..9cf35e77b71 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/SnippetTests.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Generator.CSharp.Snippets; +using NUnit.Framework; + +namespace Microsoft.Generator.CSharp.Tests +{ + public class SnippetTests + { + [Test] + public void ValidateFloat() + { + using CodeWriter writer = new CodeWriter(); + Snippet.Float(1.1f).Write(writer); + Assert.AreEqual("1.1F", writer.ToString(false)); + } + + [Test] + public void ValidateString() + { + using CodeWriter writer = new CodeWriter(); + Snippet.Literal("testing").Untyped.Write(writer); + Assert.AreEqual("\"testing\"", writer.ToString(false)); + } + + [Test] + public void ValidateStringU8() + { + using CodeWriter writer = new CodeWriter(); + Snippet.LiteralU8("testing").Untyped.Write(writer); + Assert.AreEqual("\"testing\"u8", writer.ToString(false)); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/StatementTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/StatementTests.cs index ccafae22f4b..077af7aba5a 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/StatementTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/StatementTests.cs @@ -1,10 +1,12 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; using System.Collections.Generic; using System.Linq; using Microsoft.Generator.CSharp.Expressions; +using Microsoft.Generator.CSharp.Snippets; +using Microsoft.Generator.CSharp.Statements; using NUnit.Framework; namespace Microsoft.Generator.CSharp.Tests @@ -40,8 +42,8 @@ public void AssignValueStatement() [Test] public void CreateForStatement() { - var assignment = new AssignmentExpression(new VariableReference(new CSharpType(typeof(BinaryData)), "responseParamName"), new ValueExpression()); - var condition = new BoolExpression(BoolExpression.True); + var assignment = new AssignmentExpression(new DeclarationExpression(new CSharpType(typeof(BinaryData)), new CodeWriterDeclaration("responseParamName")), new ValueExpression()); + var condition = new BoolSnippet(BoolSnippet.True); var increment = new ValueExpression(); var forStatement = new ForStatement(assignment, condition, increment); @@ -52,8 +54,8 @@ public void CreateForStatement() [Test] public void ForStatementWithAddMethod() { - var assignment = new AssignmentExpression(new VariableReference(new CSharpType(typeof(BinaryData)), "responseParamName"), new ValueExpression()); - var condition = new BoolExpression(BoolExpression.True); + var assignment = new AssignmentExpression(new DeclarationExpression(new CSharpType(typeof(BinaryData)), new CodeWriterDeclaration("responseParamName")), new ValueExpression()); + var condition = new BoolSnippet(BoolSnippet.True); var increment = new ValueExpression(); var forStatement = new ForStatement(assignment, condition, increment); var statementToAdd = new MethodBodyStatement(); @@ -100,18 +102,18 @@ public void ForeachStatementWithAddMethod() [Test] public void IfStatementWithBoolExpression() { - var condition = new BoolExpression(BoolExpression.True); + var condition = new BoolSnippet(BoolSnippet.True); var ifStatement = new IfStatement(condition); Assert.NotNull(ifStatement); - Assert.AreEqual(condition, ifStatement.Condition); + Assert.AreEqual(condition.Untyped, ifStatement.Condition); Assert.NotNull(ifStatement.Body); } [Test] public void IfStatementWithAddMethod() { - var ifStatement = new IfStatement(BoolExpression.True); + var ifStatement = new IfStatement(BoolSnippet.True); var statementToAdd = new MethodBodyStatement(); ifStatement.Add(statementToAdd); @@ -124,7 +126,7 @@ public void IfStatementWithAddMethod() [Test] public void IfStatementWithDefaultOptions() { - var condition = new BoolExpression(BoolExpression.True); + var condition = new BoolSnippet(BoolSnippet.True); var ifStatement = new IfStatement(condition); Assert.IsFalse(ifStatement.Inline); @@ -134,7 +136,7 @@ public void IfStatementWithDefaultOptions() [Test] public void IfStatementInlineOptionTrue() { - var condition = new BoolExpression(BoolExpression.True); + var condition = new BoolSnippet(BoolSnippet.True); var ifStatement = new IfStatement(condition, Inline: true); Assert.IsTrue(ifStatement.Inline); @@ -143,7 +145,7 @@ public void IfStatementInlineOptionTrue() [Test] public void IfStatementAddBracesOptionFalse() { - var condition = new BoolExpression(BoolExpression.True); + var condition = new BoolSnippet(BoolSnippet.True); var ifStatement = new IfStatement(condition, AddBraces: false); Assert.IsFalse(ifStatement.AddBraces); @@ -152,21 +154,21 @@ public void IfStatementAddBracesOptionFalse() [Test] public void IfElseStatementWithIfAndElse() { - var condition = new BoolExpression(BoolExpression.True); + var condition = new BoolSnippet(BoolSnippet.True); var elseStatement = new MethodBodyStatement(); var ifElseStatement = new IfElseStatement(new IfStatement(condition), elseStatement); Assert.NotNull(ifElseStatement); Assert.NotNull(ifElseStatement.If); - Assert.AreEqual(condition, ifElseStatement.If.Condition); + Assert.AreEqual(condition.Untyped, ifElseStatement.If.Condition); Assert.AreEqual(elseStatement, ifElseStatement.Else); } [Test] public void IfElseStatementWithConditionAndStatements() { - var condition = new BoolExpression(BoolExpression.True); + var condition = new BoolSnippet(BoolSnippet.True); var ifStatement = new MethodBodyStatement(); var elseStatement = new MethodBodyStatement(); @@ -174,7 +176,7 @@ public void IfElseStatementWithConditionAndStatements() Assert.NotNull(ifElseStatement); Assert.NotNull(ifElseStatement.If); - Assert.AreEqual(condition, ifElseStatement.If.Condition); + Assert.AreEqual(condition.Untyped, ifElseStatement.If.Condition); Assert.AreEqual(elseStatement, ifElseStatement.Else); } @@ -185,7 +187,7 @@ public void SwitchStatementWithSingleCase() var switchStatement = new SwitchStatement(matchExpression); var caseStatement = new MethodBodyStatement(); - var switchCase = new SwitchCase(new ValueExpression(), caseStatement); + var switchCase = new SwitchCaseStatement(new ValueExpression(), caseStatement); switchStatement.Add(switchCase); @@ -199,10 +201,10 @@ public void SwitchStatementWithMultipleCases() var matchExpression = new ValueExpression(); var switchStatement = new SwitchStatement(matchExpression); - var caseStatements = new List + var caseStatements = new List { - new SwitchCase(new ValueExpression(), new MethodBodyStatement()), - new SwitchCase(new ValueExpression(), new MethodBodyStatement()) + new SwitchCaseStatement(new ValueExpression(), new MethodBodyStatement()), + new SwitchCaseStatement(new ValueExpression(), new MethodBodyStatement()) }; foreach (var switchCase in caseStatements) @@ -219,10 +221,10 @@ public void SwitchStatementEnumeratingCases() var matchExpression = new ValueExpression(); var switchStatement = new SwitchStatement(matchExpression); - var caseStatements = new List + var caseStatements = new List { - new SwitchCase(new ValueExpression(), new MethodBodyStatement()), - new SwitchCase(new ValueExpression(), new MethodBodyStatement()) + new SwitchCaseStatement(new ValueExpression(), new MethodBodyStatement()), + new SwitchCaseStatement(new ValueExpression(), new MethodBodyStatement()) }; foreach (var switchCase in caseStatements) @@ -230,7 +232,7 @@ public void SwitchStatementEnumeratingCases() switchStatement.Add(switchCase); } - var enumeratedCases = new List(); + var enumeratedCases = new List(); foreach (var caseItem in switchStatement) { enumeratedCases.Add(caseItem); @@ -254,7 +256,7 @@ public void TryCatchFinallyStatementWithTryOnly() public void TryCatchFinallyStatementWithTryAndCatch() { var tryStatement = new MethodBodyStatement(); - var catchStatement = new CatchStatement(null, new MethodBodyStatement()); + var catchStatement = new CatchExpression(null, new MethodBodyStatement()); var tryCatchFinally = new TryCatchFinallyStatement(tryStatement, catchStatement, null); Assert.AreEqual(tryStatement, tryCatchFinally.Try); @@ -267,7 +269,7 @@ public void TryCatchFinallyStatementWithTryAndCatch() public void TryCatchFinallyStatementWithTryCatchAndFinally() { var tryStatement = new MethodBodyStatement(); - var catchStatement = new CatchStatement(null, new MethodBodyStatement()); + var catchStatement = new CatchExpression(null, new MethodBodyStatement()); var finallyStatement = new MethodBodyStatement(); var tryCatchFinally = new TryCatchFinallyStatement(tryStatement, catchStatement, finallyStatement); @@ -283,8 +285,8 @@ public void TryCatchFinallyStatementWithMultipleCatches() var tryStatement = new MethodBodyStatement(); var catchStatements = new[] { - new CatchStatement(null, new MethodBodyStatement()), - new CatchStatement(null, new MethodBodyStatement()) + new CatchExpression(null, new MethodBodyStatement()), + new CatchExpression(null, new MethodBodyStatement()) }; var finallyStatement = new MethodBodyStatement(); var tryCatchFinally = new TryCatchFinallyStatement(tryStatement, catchStatements, finallyStatement); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/StringExtensionsTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/StringExtensionsTests.cs index 397e50d512c..fc767973333 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/StringExtensionsTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/StringExtensionsTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using NUnit.Framework; diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TypeFactoryTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TypeFactoryTests.cs index 624838f9424..8e915d67e95 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TypeFactoryTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TypeFactoryTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Providers; using NUnit.Framework; namespace Microsoft.Generator.CSharp.Tests @@ -48,10 +49,10 @@ public static IEnumerable CreateTypeTestCases { get { - yield return new TestCaseData(new InputList("sampleType", new InputPrimitiveType(InputPrimitiveTypeKind.Boolean, false), false, false), new CSharpType(typeof(InputList), isNullable: false), false); - yield return new TestCaseData(new InputDictionary("sampleType", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), new InputPrimitiveType(InputPrimitiveTypeKind.Int32, false), false), new CSharpType(typeof(InputDictionary), isNullable: false), false); + yield return new TestCaseData(new InputListType("sampleType", new InputPrimitiveType(InputPrimitiveTypeKind.Boolean, false), false, false), new CSharpType(typeof(InputListType), isNullable: false), false); + yield return new TestCaseData(new InputDictionaryType("sampleType", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), new InputPrimitiveType(InputPrimitiveTypeKind.Int32, false), false), new CSharpType(typeof(InputDictionaryType), isNullable: false), false); yield return new TestCaseData(new InputPrimitiveType(InputPrimitiveTypeKind.String, false), new CSharpType(typeof(InputPrimitiveType), isNullable: false), false); - yield return new TestCaseData(new InputLiteralType("literalType", new InputPrimitiveType(InputPrimitiveTypeKind.String, false), "literal", false), null, true); + yield return new TestCaseData(new InputLiteralType(new InputPrimitiveType(InputPrimitiveTypeKind.String, false), "literal", false), null, true); } } @@ -78,7 +79,7 @@ public override CSharpType PageResponseType() throw new NotImplementedException(); } - public override Parameter CreateCSharpParam(InputParameter parameter) + public override ParameterProvider CreateCSharpParam(InputParameter parameter) { throw new NotImplementedException(); } @@ -92,10 +93,10 @@ public override CSharpType CreateCSharpType(InputType input) { switch (input) { - case InputList: - return new CSharpType(typeof(InputList), isNullable: false); - case InputDictionary: - return new CSharpType(typeof(InputDictionary), isNullable: false); + case InputListType: + return new CSharpType(typeof(InputListType), isNullable: false); + case InputDictionaryType: + return new CSharpType(typeof(InputDictionaryType), isNullable: false); case InputPrimitiveType: return new CSharpType(typeof(InputPrimitiveType), isNullable: false); default: diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ValueExpressionTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ValueExpressionTests.cs index 7532eaaa6aa..aaa0605b7e6 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ValueExpressionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ValueExpressionTests.cs @@ -26,7 +26,7 @@ public class ValueExpressionTests public void SystemTypeOperatorTestEqual(Type t) { ValueExpression result = t; - var expected = new TypeReference(t); + var expected = new TypeReferenceExpression(t); Assert.AreEqual(expected, result); } @@ -49,7 +49,7 @@ public void SystemTypeOperatorTestEqual(Type t) public void SystemTypeOperatorTestNotEqual(Type t1, Type t2) { ValueExpression result = t1; - var expected = new TypeReference(t2); + var expected = new TypeReferenceExpression(t2); Assert.AreNotEqual(expected, result); } @@ -58,7 +58,7 @@ public void SystemTypeOperatorTestNotEqual(Type t1, Type t2) public void CSharpTypeOperatorTestEqual(CSharpType t) { ValueExpression result = t; - var expected = new TypeReference(t); + var expected = new TypeReferenceExpression(t); Assert.AreEqual(expected, result); } @@ -67,7 +67,7 @@ public void CSharpTypeOperatorTestEqual(CSharpType t) public void CSharpTypeOperatorTestNotEqual(CSharpType t1, CSharpType t2) { ValueExpression result = t1; - var expected = new TypeReference(t2); + var expected = new TypeReferenceExpression(t2); Assert.AreNotEqual(expected, result); } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/TypeProviderWriterTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/TypeProviderWriterTests.cs index bafbaca8e0e..b73ea13a69f 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/TypeProviderWriterTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Writers/TypeProviderWriterTests.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. using System; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.SourceInput; using Moq; using NUnit.Framework; @@ -16,9 +18,8 @@ internal class TypeProviderWriterTests [SetUp] public void Setup() { - SourceInputModel? sourceInputModel = null; - var mockTypeProvider = new Mock(sourceInputModel!) { CallBase = true }; - _expressionTypeProviderWriter = new MockExpressionTypeProviderWriter(new CodeWriter(), mockTypeProvider.Object); + var mockTypeProvider = new Mock() { CallBase = true }; + _expressionTypeProviderWriter = new MockExpressionTypeProviderWriter(mockTypeProvider.Object); } // Tests that the Write method is successfully overridden. @@ -30,9 +31,9 @@ public void Write_Override() internal class MockExpressionTypeProviderWriter : TypeProviderWriter { - public MockExpressionTypeProviderWriter(CodeWriter writer, TypeProvider provider) : base(writer, provider) { } + public MockExpressionTypeProviderWriter(TypeProvider provider) : base(provider) { } - public override void Write() + public override CodeFile Write() { throw new NotImplementedException(); } diff --git a/packages/http-client-csharp/generator/Packages.Data.props b/packages/http-client-csharp/generator/Packages.Data.props index 54c25439e9c..fbe965f2df8 100644 --- a/packages/http-client-csharp/generator/Packages.Data.props +++ b/packages/http-client-csharp/generator/Packages.Data.props @@ -8,6 +8,11 @@ + + + + + diff --git a/packages/http-client-csharp/generator/TestProjects/Local/TestProjects.Local.Tests.csproj b/packages/http-client-csharp/generator/TestProjects/Local/TestProjects.Local.Tests.csproj index 356b51c4f88..0cae1828ec8 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/TestProjects.Local.Tests.csproj +++ b/packages/http-client-csharp/generator/TestProjects/Local/TestProjects.Local.Tests.csproj @@ -16,7 +16,6 @@ - diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/Unbranded-TypeSpec.tsp b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/Unbranded-TypeSpec.tsp index a083258c9da..a32908df682 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/Unbranded-TypeSpec.tsp +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/Unbranded-TypeSpec.tsp @@ -22,12 +22,20 @@ using TypeSpec.Http; using Azure.ClientGenerator.Core; using Azure.Core; +@doc("float fixed enum") +@fixed +enum FloatFixedEnumWithIntValue { + One: 1.0, + Two: 2.0, + Four: 4.0, +} + @doc("float fixed enum") @fixed enum FloatFixedEnum { - One: 1.1, - Two: 2.2, - Four: 4.4, + OneDotOne: 1.1, + TwoDotTwo: 2.2, + FourDotFour: 4.4, } @doc("int fixed enum") @@ -56,6 +64,14 @@ union IntExtensibleEnum { @doc("Float based extensible enum") union FloatExtensibleEnum { + float32, + OneDotOne: 1.1, + TwoDotTwo: 2.2, + FourDotFour: 4.4, +} + +@doc("float fixed enum") +union FloatExtensibleEnumWithIntValue { float32, One: 1.0, Two: 2.0, @@ -164,12 +180,18 @@ model RoundTripModel { @doc("this is a float based extensible enum") floatExtensibleEnum?: FloatExtensibleEnum; + @doc("this is a float based extensible enum") + floatExtensibleEnumWithIntValue?: FloatExtensibleEnumWithIntValue; + @doc("this is a collection of float based extensible enum") floatExtensibleEnumCollection?: FloatExtensibleEnum[]; @doc("this is a float based fixed enum") floatFixedEnum?: FloatFixedEnum; + @doc("this is a float based fixed enum") + floatFixedEnumWithIntValue?: FloatFixedEnumWithIntValue; + @doc("this is a collection of float based fixed enum") floatFixedEnumCollection?: FloatFixedEnum[]; @@ -220,16 +242,6 @@ union DaysOfWeekExtensibleEnum { Sunday: "Sunday", } -model ModelWithFormat { - @doc("url format") - @format("uri") - sourceUrl: string; - - @doc("uuid format") - @format("uuid") - guid: string; -} - @route("/hello") @doc("Return hi") @get @@ -282,7 +294,7 @@ op helloLiteral(@header p1: "test", @path p2: 123, @query p3: true): Thing; @doc("top level method") @get @convenientAPI(true) -op topAction(@path @format("date") action: string): Thing; +op topAction(@path action: utcDateTime): Thing; @route("/top2") @doc("top level method2") @@ -310,12 +322,6 @@ op friendlyModel(...NotFriend): NotFriend; op addTimeHeader(@header("Repeatability-First-Sent") repeatabilityFirstSent?: utcDateTime): void; -@route("/stringFormat") -@doc("parameter has string format.") -@post -@convenientAPI(true) -op stringFormat(@path @format("uuid") subscriptionId: string, @body body: ModelWithFormat): void; - @route("/projectedName") @doc("Model can have its projected name") @post diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Internal/Argument.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Internal/Argument.cs new file mode 100644 index 00000000000..212a9516920 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Internal/Argument.cs @@ -0,0 +1,152 @@ +// + +#nullable disable + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace UnbrandedTypeSpec +{ + internal static partial class Argument + { + /// The value. + /// The name. + public static void AssertNotNull(T value, string name) + { + if (value is null) + { + throw new ArgumentNullException(name); + } + } + + /// The value. + /// The name. + public static void AssertNotNull(T? value, string name) + where T : struct + { + if (!value.HasValue) + { + throw new ArgumentNullException(name); + } + } + + /// The value. + /// The name. + public static void AssertNotNullOrEmpty(IEnumerable value, string name) + { + if (value is null) + { + throw new ArgumentNullException(name); + } + if (value is ICollection collectionOfT && collectionOfT.Count == 0) + { + throw new ArgumentException("Value cannot be an empty collection.", name); + } + if (value is ICollection collection && collection.Count == 0) + { + throw new ArgumentException("Value cannot be an empty collection.", name); + } + using IEnumerator e = value.GetEnumerator(); + if (!e.MoveNext()) + { + throw new ArgumentException("Value cannot be an empty collection.", name); + } + } + + /// The value. + /// The name. + public static void AssertNotNullOrEmpty(string value, string name) + { + if (value is null) + { + throw new ArgumentNullException(name); + } + if (value.Length == 0) + { + throw new ArgumentException("Value cannot be an empty string.", name); + } + } + + /// The value. + /// The name. + public static void AssertNotNullOrWhiteSpace(string value, string name) + { + if (value is null) + { + throw new ArgumentNullException(name); + } + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Value cannot be empty or contain only white-space characters.", name); + } + } + + /// The value. + /// The name. + public static void AssertNotDefault(ref T value, string name) + where T : struct, IEquatable + { + if (value.Equals(default)) + { + throw new ArgumentException("Value cannot be empty.", name); + } + } + + /// The value. + /// The minimum value. + /// The maximum value. + /// The name. + public static void AssertInRange(T value, T minimum, T maximum, string name) + where T : notnull, IComparable + { + if (minimum.CompareTo(value) > 0) + { + throw new ArgumentOutOfRangeException(name, "Value is less than the minimum allowed."); + } + if (maximum.CompareTo(value) < 0) + { + throw new ArgumentOutOfRangeException(name, "Value is greater than the maximum allowed."); + } + } + + /// The enum value. + /// The value. + /// The name. + public static void AssertEnumDefined(Type enumType, object value, string name) + { + if (!Enum.IsDefined(enumType, value)) + { + throw new ArgumentException($"Value not defined for {enumType.FullName}.", name); + } + } + + /// The value. + /// The name. + public static T CheckNotNull(T value, string name) + where T : class + { + AssertNotNull(value, name); + return value; + } + + /// The value. + /// The name. + public static string CheckNotNullOrEmpty(string value, string name) + { + AssertNotNullOrEmpty(value, name); + return value; + } + + /// The value. + /// The name. + /// The message. + public static void AssertNull(T value, string name, string message = null) + { + if (value != null) + { + throw new ArgumentException(message ?? "Value must be null.", name); + } + } + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Internal/ChangeTrackingDictionary.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Internal/ChangeTrackingDictionary.cs new file mode 100644 index 00000000000..44115fb65e4 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Internal/ChangeTrackingDictionary.cs @@ -0,0 +1,186 @@ +// + +#nullable disable + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace UnbrandedTypeSpec +{ + internal partial class ChangeTrackingDictionary : IDictionary, IReadOnlyDictionary + where TKey : notnull + { + private IDictionary _innerDictionary; + + public ChangeTrackingDictionary() + { + } + + /// The inner dictionary. + public ChangeTrackingDictionary(IDictionary dictionary) + { + if (dictionary == null) + { + return; + } + _innerDictionary = new Dictionary(dictionary); + } + + /// The inner dictionary. + public ChangeTrackingDictionary(IReadOnlyDictionary dictionary) + { + if (dictionary == null) + { + return; + } + _innerDictionary = new Dictionary(); + foreach (var pair in dictionary) + { + _innerDictionary.Add(pair); + } + } + + /// Gets the isundefined. + public bool IsUndefined => _innerDictionary == null; + + /// Gets the count. + public int Count => IsUndefined ? 0 : EnsureDictionary().Count; + + /// Gets the IsReadOnly. + public bool IsReadOnly => IsUndefined ? false : EnsureDictionary().IsReadOnly; + + /// Gets the keys. + public ICollection Keys => IsUndefined ? Array.Empty() : EnsureDictionary().Keys; + + /// Gets the values. + public ICollection Values => IsUndefined ? Array.Empty() : EnsureDictionary().Values; + + /// Gets or sets the this. + public TValue this[TKey key] + { + get + { + if (IsUndefined) + { + throw new KeyNotFoundException(nameof(key)); + } + return EnsureDictionary()[key]; + } + set + { + EnsureDictionary()[key] = value; + } + } + + /// Gets the keys. + IEnumerable IReadOnlyDictionary.Keys => Keys; + + /// Gets the values. + IEnumerable IReadOnlyDictionary.Values => Values; + + public IEnumerator> GetEnumerator() + { + if (IsUndefined) + { + IEnumerator> enumerateEmpty() + { + yield break; + } + return enumerateEmpty(); + } + return EnsureDictionary().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// The item to add. + public void Add(KeyValuePair item) + { + EnsureDictionary().Add(item); + } + + public void Clear() + { + EnsureDictionary().Clear(); + } + + /// The item to search for. + public bool Contains(KeyValuePair item) + { + if (IsUndefined) + { + return false; + } + return EnsureDictionary().Contains(item); + } + + /// The array to copy. + /// The index. + public void CopyTo(KeyValuePair[] array, int index) + { + if (IsUndefined) + { + return; + } + EnsureDictionary().CopyTo(array, index); + } + + /// The item to remove. + public bool Remove(KeyValuePair item) + { + if (IsUndefined) + { + return false; + } + return EnsureDictionary().Remove(item); + } + + /// The key. + /// The value to add. + public void Add(TKey key, TValue value) + { + EnsureDictionary().Add(key, value); + } + + /// The key to search for. + public bool ContainsKey(TKey key) + { + if (IsUndefined) + { + return false; + } + return EnsureDictionary().ContainsKey(key); + } + + /// The key. + public bool Remove(TKey key) + { + if (IsUndefined) + { + return false; + } + return EnsureDictionary().Remove(key); + } + + /// The key to search for. + /// The value. + public bool TryGetValue(TKey key, out TValue value) + { + if (IsUndefined) + { + value = default; + return false; + } + return EnsureDictionary().TryGetValue(key, out value); + } + + public IDictionary EnsureDictionary() + { + return _innerDictionary ??= new Dictionary(); + } + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Internal/ChangeTrackingList.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Internal/ChangeTrackingList.cs new file mode 100644 index 00000000000..9e2ba9951e2 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Internal/ChangeTrackingList.cs @@ -0,0 +1,165 @@ +// + +#nullable disable + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace UnbrandedTypeSpec +{ + internal partial class ChangeTrackingList : IList, IReadOnlyList + { + private IList _innerList; + + public ChangeTrackingList() + { + } + + /// The inner list. + public ChangeTrackingList(IList innerList) + { + if (innerList != null) + { + _innerList = innerList; + } + } + + /// The inner list. + public ChangeTrackingList(IReadOnlyList innerList) + { + if (innerList != null) + { + _innerList = innerList.ToList(); + } + } + + /// Gets the isundefined. + public bool IsUndefined => _innerList == null; + + /// Gets the count. + public int Count => IsUndefined ? 0 : EnsureList().Count; + + /// Gets the IsReadOnly. + public bool IsReadOnly => IsUndefined ? false : EnsureList().IsReadOnly; + + /// Gets or sets the this. + public T this[int index] + { + get + { + if (IsUndefined) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + return EnsureList()[index]; + } + set + { + if (IsUndefined) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + EnsureList()[index] = value; + } + } + + public void Reset() + { + _innerList = null; + } + + public IEnumerator GetEnumerator() + { + if (IsUndefined) + { + IEnumerator enumerateEmpty() + { + yield break; + } + return enumerateEmpty(); + } + return EnsureList().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// The item to add. + public void Add(T item) + { + EnsureList().Add(item); + } + + public void Clear() + { + EnsureList().Clear(); + } + + /// The item. + public bool Contains(T item) + { + if (IsUndefined) + { + return false; + } + return EnsureList().Contains(item); + } + + /// The array to copy to. + /// The array index. + public void CopyTo(T[] array, int arrayIndex) + { + if (IsUndefined) + { + return; + } + EnsureList().CopyTo(array, arrayIndex); + } + + /// The item. + public bool Remove(T item) + { + if (IsUndefined) + { + return false; + } + return EnsureList().Remove(item); + } + + /// The item. + public int IndexOf(T item) + { + if (IsUndefined) + { + return -1; + } + return EnsureList().IndexOf(item); + } + + /// The index. + /// The item. + public void Insert(int index, T item) + { + EnsureList().Insert(index, item); + } + + /// The index. + public void RemoveAt(int index) + { + if (IsUndefined) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + EnsureList().RemoveAt(index); + } + + public IList EnsureList() + { + return _innerList ??= new List(); + } + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Internal/Optional.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Internal/Optional.cs new file mode 100644 index 00000000000..7a247f4bd19 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Internal/Optional.cs @@ -0,0 +1,55 @@ +// + +#nullable disable + +using System.Collections.Generic; +using System.Text.Json; + +namespace UnbrandedTypeSpec +{ + internal static partial class Optional + { + /// The collection. + public static bool IsCollectionDefined(IEnumerable collection) + { + return !(collection is ChangeTrackingList changeTrackingList && changeTrackingList.IsUndefined); + } + + /// The collection. + public static bool IsCollectionDefined(IDictionary collection) + { + return !(collection is ChangeTrackingDictionary changeTrackingDictionary && changeTrackingDictionary.IsUndefined); + } + + /// The value. + public static bool IsCollectionDefined(IReadOnlyDictionary collection) + { + return !(collection is ChangeTrackingDictionary changeTrackingDictionary && changeTrackingDictionary.IsUndefined); + } + + /// The value. + public static bool IsDefined(T? value) + where T : struct + { + return value.HasValue; + } + + /// The value. + public static bool IsDefined(object value) + { + return value != null; + } + + /// The value. + public static bool IsDefined(System.Text.Json.JsonElement value) + { + return value.ValueKind != System.Text.Json.JsonValueKind.Undefined; + } + + /// The value. + public static bool IsDefined(string value) + { + return value != null; + } + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/FloatExtensibleEnum.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/FloatExtensibleEnum.cs new file mode 100644 index 00000000000..088bac8817a --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/FloatExtensibleEnum.cs @@ -0,0 +1,61 @@ +// + +#nullable disable + +using System; +using System.ComponentModel; +using System.Globalization; + +namespace UnbrandedTypeSpec.Models +{ + public readonly partial struct FloatExtensibleEnum : IEquatable + { + private readonly float _value; + private const float OneDotOneValue = 1.1F; + private const float TwoDotTwoValue = 2.2F; + private const float FourDotFourValue = 4.4F; + + /// Initializes a new instance of . + /// The value. + public FloatExtensibleEnum(float value) + { + _value = value; + } + + /// Gets the onedotone. + public static FloatExtensibleEnum OneDotOne { get; } = new FloatExtensibleEnum(OneDotOneValue); + + /// Gets the twodottwo. + public static FloatExtensibleEnum TwoDotTwo { get; } = new FloatExtensibleEnum(TwoDotTwoValue); + + /// Gets the fourdotfour. + public static FloatExtensibleEnum FourDotFour { get; } = new FloatExtensibleEnum(FourDotFourValue); + + /// Determines if two values are the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator ==(FloatExtensibleEnum left, FloatExtensibleEnum right) => left.Equals(right); + + /// Determines if two values are not the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator !=(FloatExtensibleEnum left, FloatExtensibleEnum right) => !left.Equals(right); + + /// Converts a string to a . + /// The value. + public static implicit operator FloatExtensibleEnum(float value) => new FloatExtensibleEnum(value); + + /// The object to compare. + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => obj is FloatExtensibleEnum other && Equals(other); + + /// The instance to compare. + public bool Equals(FloatExtensibleEnum other) => Equals(_value, other._value); + + public override int GetHashCode() => _value.GetHashCode(); + + public override string ToString() => _value.ToString(CultureInfo.InvariantCulture); + + internal float ToSerialSingle() => _value; + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/FloatExtensibleEnumWithIntValue.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/FloatExtensibleEnumWithIntValue.cs new file mode 100644 index 00000000000..8f4fbb58c14 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/FloatExtensibleEnumWithIntValue.cs @@ -0,0 +1,61 @@ +// + +#nullable disable + +using System; +using System.ComponentModel; +using System.Globalization; + +namespace UnbrandedTypeSpec.Models +{ + public readonly partial struct FloatExtensibleEnumWithIntValue : IEquatable + { + private readonly float _value; + private const float OneValue = 1F; + private const float TwoValue = 2F; + private const float FourValue = 4F; + + /// Initializes a new instance of . + /// The value. + public FloatExtensibleEnumWithIntValue(float value) + { + _value = value; + } + + /// Gets the one. + public static FloatExtensibleEnumWithIntValue One { get; } = new FloatExtensibleEnumWithIntValue(OneValue); + + /// Gets the two. + public static FloatExtensibleEnumWithIntValue Two { get; } = new FloatExtensibleEnumWithIntValue(TwoValue); + + /// Gets the four. + public static FloatExtensibleEnumWithIntValue Four { get; } = new FloatExtensibleEnumWithIntValue(FourValue); + + /// Determines if two values are the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator ==(FloatExtensibleEnumWithIntValue left, FloatExtensibleEnumWithIntValue right) => left.Equals(right); + + /// Determines if two values are not the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator !=(FloatExtensibleEnumWithIntValue left, FloatExtensibleEnumWithIntValue right) => !left.Equals(right); + + /// Converts a string to a . + /// The value. + public static implicit operator FloatExtensibleEnumWithIntValue(float value) => new FloatExtensibleEnumWithIntValue(value); + + /// The object to compare. + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => obj is FloatExtensibleEnumWithIntValue other && Equals(other); + + /// The instance to compare. + public bool Equals(FloatExtensibleEnumWithIntValue other) => Equals(_value, other._value); + + public override int GetHashCode() => _value.GetHashCode(); + + public override string ToString() => _value.ToString(CultureInfo.InvariantCulture); + + internal float ToSerialSingle() => _value; + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/FloatFixedEnum.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/FloatFixedEnum.cs new file mode 100644 index 00000000000..6411a99c046 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/FloatFixedEnum.cs @@ -0,0 +1,13 @@ +// + +#nullable disable + +namespace UnbrandedTypeSpec.Models +{ + public enum FloatFixedEnum + { + OneDotOne, + TwoDotTwo, + FourDotFour + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/FloatFixedEnumExtensions.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/FloatFixedEnumExtensions.cs new file mode 100644 index 00000000000..e5809cd7e0d --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/FloatFixedEnumExtensions.cs @@ -0,0 +1,38 @@ +// + +#nullable disable + +using System; + +namespace UnbrandedTypeSpec.Models +{ + internal static partial class FloatFixedEnumExtensions + { + /// The value to serialize. + public static float ToSerialSingle(this FloatFixedEnum value) => value switch + { + FloatFixedEnum.OneDotOne => 1.1F, + FloatFixedEnum.TwoDotTwo => 2.2F, + FloatFixedEnum.FourDotFour => 4.4F, + _ => throw new ArgumentOutOfRangeException(nameof(value), value, "Unknown FloatFixedEnum value.") + }; + + /// The value to deserialize. + public static FloatFixedEnum ToFloatFixedEnum(this float value) + { + if (value == 1.1F) + { + return FloatFixedEnum.OneDotOne; + } + if (value == 2.2F) + { + return FloatFixedEnum.TwoDotTwo; + } + if (value == 4.4F) + { + return FloatFixedEnum.FourDotFour; + } + throw new ArgumentOutOfRangeException(nameof(value), value, "Unknown FloatFixedEnum value."); + } + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/FloatFixedEnumWithIntValue.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/FloatFixedEnumWithIntValue.cs new file mode 100644 index 00000000000..7b00b4499f9 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/FloatFixedEnumWithIntValue.cs @@ -0,0 +1,13 @@ +// + +#nullable disable + +namespace UnbrandedTypeSpec.Models +{ + public enum FloatFixedEnumWithIntValue + { + One = 1, + Two = 2, + Four = 4 + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/FloatFixedEnumWithIntValueExtensions.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/FloatFixedEnumWithIntValueExtensions.cs new file mode 100644 index 00000000000..f182b74821e --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/FloatFixedEnumWithIntValueExtensions.cs @@ -0,0 +1,29 @@ +// + +#nullable disable + +using System; + +namespace UnbrandedTypeSpec.Models +{ + internal static partial class FloatFixedEnumWithIntValueExtensions + { + /// The value to deserialize. + public static FloatFixedEnumWithIntValue ToFloatFixedEnumWithIntValue(this int value) + { + if (value == 1) + { + return FloatFixedEnumWithIntValue.One; + } + if (value == 2) + { + return FloatFixedEnumWithIntValue.Two; + } + if (value == 4) + { + return FloatFixedEnumWithIntValue.Four; + } + throw new ArgumentOutOfRangeException(nameof(value), value, "Unknown FloatFixedEnumWithIntValue value."); + } + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/Friend.Serialization.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/Friend.Serialization.cs index 9453e3f88ab..1e80e75d8e7 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/Friend.Serialization.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/Friend.Serialization.cs @@ -4,31 +4,56 @@ using System; using System.ClientModel.Primitives; +using System.Collections.Generic; using System.Text.Json; namespace UnbrandedTypeSpec.Models { public partial class Friend : System.ClientModel.Primitives.IJsonModel { + private IDictionary _serializedAdditionalRawData; + + /// Initializes a new instance of . + /// name of the NotFriend. + /// Keeps track of any properties unknown to the library. + internal Friend(string name, IDictionary serializedAdditionalRawData) + { + Name = name; + _serializedAdditionalRawData = serializedAdditionalRawData; + } + + /// Initializes a new instance of for deserialization. + internal Friend() + { + } + + /// The JSON writer. + /// The client options for reading and writing models. void System.ClientModel.Primitives.IJsonModel.Write(System.Text.Json.Utf8JsonWriter writer, System.ClientModel.Primitives.ModelReaderWriterOptions options) { } + /// The JSON reader. + /// The client options for reading and writing models. Friend System.ClientModel.Primitives.IJsonModel.Create(ref System.Text.Json.Utf8JsonReader reader, System.ClientModel.Primitives.ModelReaderWriterOptions options) { return new Friend(); } + /// The client options for reading and writing models. System.BinaryData System.ClientModel.Primitives.IPersistableModel.Write(System.ClientModel.Primitives.ModelReaderWriterOptions options) { return new System.BinaryData("IPersistableModel"); } + /// The data to parse. + /// The client options for reading and writing models. Friend System.ClientModel.Primitives.IPersistableModel.Create(System.BinaryData data, System.ClientModel.Primitives.ModelReaderWriterOptions options) { return new Friend(); } + /// The client options for reading and writing models. string System.ClientModel.Primitives.IPersistableModel.GetFormatFromOptions(System.ClientModel.Primitives.ModelReaderWriterOptions options) => "J"; } } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/Friend.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/Friend.cs index ebe61299679..f13f97e7da1 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/Friend.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/Friend.cs @@ -3,6 +3,7 @@ #nullable disable using System; +using UnbrandedTypeSpec; namespace UnbrandedTypeSpec.Models { @@ -13,19 +14,11 @@ public partial class Friend /// is null. public Friend(string name) { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } + Argument.AssertNotNull(name, nameof(name)); Name = name; } - /// Initializes a new instance of for deserialization. - internal Friend() - { - } - /// name of the NotFriend. public string Name { get; set; } } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/IntExtensibleEnum.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/IntExtensibleEnum.cs new file mode 100644 index 00000000000..7c8b4561297 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/IntExtensibleEnum.cs @@ -0,0 +1,61 @@ +// + +#nullable disable + +using System; +using System.ComponentModel; +using System.Globalization; + +namespace UnbrandedTypeSpec.Models +{ + public readonly partial struct IntExtensibleEnum : IEquatable + { + private readonly int _value; + private const int OneValue = 1; + private const int TwoValue = 2; + private const int FourValue = 4; + + /// Initializes a new instance of . + /// The value. + public IntExtensibleEnum(int value) + { + _value = value; + } + + /// Gets the one. + public static IntExtensibleEnum One { get; } = new IntExtensibleEnum(OneValue); + + /// Gets the two. + public static IntExtensibleEnum Two { get; } = new IntExtensibleEnum(TwoValue); + + /// Gets the four. + public static IntExtensibleEnum Four { get; } = new IntExtensibleEnum(FourValue); + + /// Determines if two values are the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator ==(IntExtensibleEnum left, IntExtensibleEnum right) => left.Equals(right); + + /// Determines if two values are not the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator !=(IntExtensibleEnum left, IntExtensibleEnum right) => !left.Equals(right); + + /// Converts a string to a . + /// The value. + public static implicit operator IntExtensibleEnum(int value) => new IntExtensibleEnum(value); + + /// The object to compare. + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => obj is IntExtensibleEnum other && Equals(other); + + /// The instance to compare. + public bool Equals(IntExtensibleEnum other) => Equals(_value, other._value); + + public override int GetHashCode() => _value.GetHashCode(); + + public override string ToString() => _value.ToString(CultureInfo.InvariantCulture); + + internal int ToSerialInt32() => _value; + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/IntFixedEnum.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/IntFixedEnum.cs new file mode 100644 index 00000000000..7468e932451 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/IntFixedEnum.cs @@ -0,0 +1,13 @@ +// + +#nullable disable + +namespace UnbrandedTypeSpec.Models +{ + public enum IntFixedEnum + { + One = 1, + Two = 2, + Four = 4 + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/IntFixedEnumExtensions.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/IntFixedEnumExtensions.cs new file mode 100644 index 00000000000..f94052f4703 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/IntFixedEnumExtensions.cs @@ -0,0 +1,29 @@ +// + +#nullable disable + +using System; + +namespace UnbrandedTypeSpec.Models +{ + internal static partial class IntFixedEnumExtensions + { + /// The value to deserialize. + public static IntFixedEnum ToIntFixedEnum(this int value) + { + if (value == 1) + { + return IntFixedEnum.One; + } + if (value == 2) + { + return IntFixedEnum.Two; + } + if (value == 4) + { + return IntFixedEnum.Four; + } + throw new ArgumentOutOfRangeException(nameof(value), value, "Unknown IntFixedEnum value."); + } + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ModelWithFormat.Serialization.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ModelWithFormat.Serialization.cs deleted file mode 100644 index 2ea3feb37e4..00000000000 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ModelWithFormat.Serialization.cs +++ /dev/null @@ -1,34 +0,0 @@ -// - -#nullable disable - -using System; -using System.ClientModel.Primitives; -using System.Text.Json; - -namespace UnbrandedTypeSpec.Models -{ - public partial class ModelWithFormat : System.ClientModel.Primitives.IJsonModel - { - void System.ClientModel.Primitives.IJsonModel.Write(System.Text.Json.Utf8JsonWriter writer, System.ClientModel.Primitives.ModelReaderWriterOptions options) - { - } - - ModelWithFormat System.ClientModel.Primitives.IJsonModel.Create(ref System.Text.Json.Utf8JsonReader reader, System.ClientModel.Primitives.ModelReaderWriterOptions options) - { - return new ModelWithFormat(); - } - - System.BinaryData System.ClientModel.Primitives.IPersistableModel.Write(System.ClientModel.Primitives.ModelReaderWriterOptions options) - { - return new System.BinaryData("IPersistableModel"); - } - - ModelWithFormat System.ClientModel.Primitives.IPersistableModel.Create(System.BinaryData data, System.ClientModel.Primitives.ModelReaderWriterOptions options) - { - return new ModelWithFormat(); - } - - string System.ClientModel.Primitives.IPersistableModel.GetFormatFromOptions(System.ClientModel.Primitives.ModelReaderWriterOptions options) => "J"; - } -} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ModelWithFormat.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ModelWithFormat.cs deleted file mode 100644 index 233a37c3fb9..00000000000 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ModelWithFormat.cs +++ /dev/null @@ -1,37 +0,0 @@ -// - -#nullable disable - -using System; - -namespace UnbrandedTypeSpec.Models -{ - public partial class ModelWithFormat - { - /// Initializes a new instance of . - /// url format. - /// uuid format. - /// is null. - public ModelWithFormat(System.Uri sourceUrl, Guid guid) - { - if (sourceUrl == null) - { - throw new ArgumentNullException(nameof(sourceUrl)); - } - - SourceUrl = sourceUrl; - Guid = guid; - } - - /// Initializes a new instance of for deserialization. - internal ModelWithFormat() - { - } - - /// url format. - public System.Uri SourceUrl { get; set; } - - /// uuid format. - public Guid Guid { get; set; } - } -} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ModelWithRequiredNullableProperties.Serialization.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ModelWithRequiredNullableProperties.Serialization.cs index e3a415aef9a..e0abfbe4c3a 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ModelWithRequiredNullableProperties.Serialization.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ModelWithRequiredNullableProperties.Serialization.cs @@ -4,31 +4,60 @@ using System; using System.ClientModel.Primitives; +using System.Collections.Generic; using System.Text.Json; namespace UnbrandedTypeSpec.Models { public partial class ModelWithRequiredNullableProperties : System.ClientModel.Primitives.IJsonModel { + private IDictionary _serializedAdditionalRawData; + + /// Initializes a new instance of . + /// required nullable primitive type. + /// required nullable extensible enum type. + /// required nullable fixed enum type. + /// Keeps track of any properties unknown to the library. + internal ModelWithRequiredNullableProperties(int? requiredNullablePrimitive, StringExtensibleEnum? requiredExtensibleEnum, StringFixedEnum? requiredFixedEnum, IDictionary serializedAdditionalRawData) + { + RequiredNullablePrimitive = requiredNullablePrimitive; + RequiredExtensibleEnum = requiredExtensibleEnum; + RequiredFixedEnum = requiredFixedEnum; + _serializedAdditionalRawData = serializedAdditionalRawData; + } + + /// Initializes a new instance of for deserialization. + internal ModelWithRequiredNullableProperties() + { + } + + /// The JSON writer. + /// The client options for reading and writing models. void System.ClientModel.Primitives.IJsonModel.Write(System.Text.Json.Utf8JsonWriter writer, System.ClientModel.Primitives.ModelReaderWriterOptions options) { } + /// The JSON reader. + /// The client options for reading and writing models. ModelWithRequiredNullableProperties System.ClientModel.Primitives.IJsonModel.Create(ref System.Text.Json.Utf8JsonReader reader, System.ClientModel.Primitives.ModelReaderWriterOptions options) { return new ModelWithRequiredNullableProperties(); } + /// The client options for reading and writing models. System.BinaryData System.ClientModel.Primitives.IPersistableModel.Write(System.ClientModel.Primitives.ModelReaderWriterOptions options) { return new System.BinaryData("IPersistableModel"); } + /// The data to parse. + /// The client options for reading and writing models. ModelWithRequiredNullableProperties System.ClientModel.Primitives.IPersistableModel.Create(System.BinaryData data, System.ClientModel.Primitives.ModelReaderWriterOptions options) { return new ModelWithRequiredNullableProperties(); } + /// The client options for reading and writing models. string System.ClientModel.Primitives.IPersistableModel.GetFormatFromOptions(System.ClientModel.Primitives.ModelReaderWriterOptions options) => "J"; } } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ModelWithRequiredNullableProperties.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ModelWithRequiredNullableProperties.cs index ee30c18d367..5db2cccc843 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ModelWithRequiredNullableProperties.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ModelWithRequiredNullableProperties.cs @@ -10,25 +10,20 @@ public partial class ModelWithRequiredNullableProperties /// required nullable primitive type. /// required nullable extensible enum type. /// required nullable fixed enum type. - public ModelWithRequiredNullableProperties(int? requiredNullablePrimitive, string requiredExtensibleEnum, string requiredFixedEnum) + public ModelWithRequiredNullableProperties(int? requiredNullablePrimitive, StringExtensibleEnum? requiredExtensibleEnum, StringFixedEnum? requiredFixedEnum) { RequiredNullablePrimitive = requiredNullablePrimitive; RequiredExtensibleEnum = requiredExtensibleEnum; RequiredFixedEnum = requiredFixedEnum; } - /// Initializes a new instance of for deserialization. - internal ModelWithRequiredNullableProperties() - { - } - /// required nullable primitive type. public int? RequiredNullablePrimitive { get; set; } /// required nullable extensible enum type. - public string RequiredExtensibleEnum { get; set; } + public StringExtensibleEnum? RequiredExtensibleEnum { get; set; } /// required nullable fixed enum type. - public string RequiredFixedEnum { get; set; } + public StringFixedEnum? RequiredFixedEnum { get; set; } } } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ProjectedModel.Serialization.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ProjectedModel.Serialization.cs index e06f11ceb47..2ffe34d0830 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ProjectedModel.Serialization.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ProjectedModel.Serialization.cs @@ -4,31 +4,56 @@ using System; using System.ClientModel.Primitives; +using System.Collections.Generic; using System.Text.Json; namespace UnbrandedTypeSpec.Models { public partial class ProjectedModel : System.ClientModel.Primitives.IJsonModel { + private IDictionary _serializedAdditionalRawData; + + /// Initializes a new instance of . + /// name of the ModelWithProjectedName. + /// Keeps track of any properties unknown to the library. + internal ProjectedModel(string name, IDictionary serializedAdditionalRawData) + { + Name = name; + _serializedAdditionalRawData = serializedAdditionalRawData; + } + + /// Initializes a new instance of for deserialization. + internal ProjectedModel() + { + } + + /// The JSON writer. + /// The client options for reading and writing models. void System.ClientModel.Primitives.IJsonModel.Write(System.Text.Json.Utf8JsonWriter writer, System.ClientModel.Primitives.ModelReaderWriterOptions options) { } + /// The JSON reader. + /// The client options for reading and writing models. ProjectedModel System.ClientModel.Primitives.IJsonModel.Create(ref System.Text.Json.Utf8JsonReader reader, System.ClientModel.Primitives.ModelReaderWriterOptions options) { return new ProjectedModel(); } + /// The client options for reading and writing models. System.BinaryData System.ClientModel.Primitives.IPersistableModel.Write(System.ClientModel.Primitives.ModelReaderWriterOptions options) { return new System.BinaryData("IPersistableModel"); } + /// The data to parse. + /// The client options for reading and writing models. ProjectedModel System.ClientModel.Primitives.IPersistableModel.Create(System.BinaryData data, System.ClientModel.Primitives.ModelReaderWriterOptions options) { return new ProjectedModel(); } + /// The client options for reading and writing models. string System.ClientModel.Primitives.IPersistableModel.GetFormatFromOptions(System.ClientModel.Primitives.ModelReaderWriterOptions options) => "J"; } } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ProjectedModel.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ProjectedModel.cs index d7c32bd1e75..5d734f9c5b3 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ProjectedModel.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ProjectedModel.cs @@ -3,6 +3,7 @@ #nullable disable using System; +using UnbrandedTypeSpec; namespace UnbrandedTypeSpec.Models { @@ -13,19 +14,11 @@ public partial class ProjectedModel /// is null. public ProjectedModel(string name) { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } + Argument.AssertNotNull(name, nameof(name)); Name = name; } - /// Initializes a new instance of for deserialization. - internal ProjectedModel() - { - } - /// name of the ModelWithProjectedName. public string Name { get; set; } } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ReturnsAnonymousModelResponse.Serialization.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ReturnsAnonymousModelResponse.Serialization.cs index a0e4b7f6314..43143217070 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ReturnsAnonymousModelResponse.Serialization.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ReturnsAnonymousModelResponse.Serialization.cs @@ -4,31 +4,49 @@ using System; using System.ClientModel.Primitives; +using System.Collections.Generic; using System.Text.Json; namespace UnbrandedTypeSpec.Models { public partial class ReturnsAnonymousModelResponse : System.ClientModel.Primitives.IJsonModel { + private IDictionary _serializedAdditionalRawData; + + /// Initializes a new instance of . + /// Keeps track of any properties unknown to the library. + internal ReturnsAnonymousModelResponse(IDictionary serializedAdditionalRawData) + { + _serializedAdditionalRawData = serializedAdditionalRawData; + } + + /// The JSON writer. + /// The client options for reading and writing models. void System.ClientModel.Primitives.IJsonModel.Write(System.Text.Json.Utf8JsonWriter writer, System.ClientModel.Primitives.ModelReaderWriterOptions options) { } + /// The JSON reader. + /// The client options for reading and writing models. ReturnsAnonymousModelResponse System.ClientModel.Primitives.IJsonModel.Create(ref System.Text.Json.Utf8JsonReader reader, System.ClientModel.Primitives.ModelReaderWriterOptions options) { return new ReturnsAnonymousModelResponse(); } + /// The client options for reading and writing models. System.BinaryData System.ClientModel.Primitives.IPersistableModel.Write(System.ClientModel.Primitives.ModelReaderWriterOptions options) { return new System.BinaryData("IPersistableModel"); } + /// The data to parse. + /// The client options for reading and writing models. ReturnsAnonymousModelResponse System.ClientModel.Primitives.IPersistableModel.Create(System.BinaryData data, System.ClientModel.Primitives.ModelReaderWriterOptions options) { return new ReturnsAnonymousModelResponse(); } + /// The client options for reading and writing models. string System.ClientModel.Primitives.IPersistableModel.GetFormatFromOptions(System.ClientModel.Primitives.ModelReaderWriterOptions options) => "J"; } } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/RoundTripModel.Serialization.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/RoundTripModel.Serialization.cs index d32dc9ce57b..59c50b090dd 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/RoundTripModel.Serialization.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/RoundTripModel.Serialization.cs @@ -4,31 +4,102 @@ using System; using System.ClientModel.Primitives; +using System.Collections.Generic; using System.Text.Json; namespace UnbrandedTypeSpec.Models { public partial class RoundTripModel : System.ClientModel.Primitives.IJsonModel { + private IDictionary _serializedAdditionalRawData; + + /// Initializes a new instance of . + /// Required string, illustrating a reference type property. + /// Required int, illustrating a value type property. + /// Required collection of enums. + /// Required dictionary of enums. + /// Required model. + /// this is an int based extensible enum. + /// this is a collection of int based extensible enum. + /// this is a float based extensible enum. + /// this is a float based extensible enum. + /// this is a collection of float based extensible enum. + /// this is a float based fixed enum. + /// this is a float based fixed enum. + /// this is a collection of float based fixed enum. + /// this is a int based fixed enum. + /// this is a collection of int based fixed enum. + /// this is a string based fixed enum. + /// required unknown. + /// optional unknown. + /// required record of unknown. + /// optional record of unknown. + /// required readonly record of unknown. + /// optional readonly record of unknown. + /// this is a model with required nullable properties. + /// Required bytes. + /// Keeps track of any properties unknown to the library. + internal RoundTripModel(string requiredString, int requiredInt, IList requiredCollection, IDictionary requiredDictionary, Thing requiredModel, IntExtensibleEnum intExtensibleEnum, IList intExtensibleEnumCollection, FloatExtensibleEnum floatExtensibleEnum, FloatExtensibleEnumWithIntValue floatExtensibleEnumWithIntValue, IList floatExtensibleEnumCollection, FloatFixedEnum floatFixedEnum, FloatFixedEnumWithIntValue floatFixedEnumWithIntValue, IList floatFixedEnumCollection, IntFixedEnum intFixedEnum, IList intFixedEnumCollection, StringFixedEnum? stringFixedEnum, System.BinaryData requiredUnknown, System.BinaryData optionalUnknown, IDictionary requiredRecordUnknown, IDictionary optionalRecordUnknown, IDictionary readOnlyRequiredRecordUnknown, IDictionary readOnlyOptionalRecordUnknown, ModelWithRequiredNullableProperties modelWithRequiredNullable, System.BinaryData requiredBytes, IDictionary serializedAdditionalRawData) + { + RequiredString = requiredString; + RequiredInt = requiredInt; + RequiredCollection = requiredCollection; + RequiredDictionary = requiredDictionary; + RequiredModel = requiredModel; + IntExtensibleEnum = intExtensibleEnum; + IntExtensibleEnumCollection = intExtensibleEnumCollection; + FloatExtensibleEnum = floatExtensibleEnum; + FloatExtensibleEnumWithIntValue = floatExtensibleEnumWithIntValue; + FloatExtensibleEnumCollection = floatExtensibleEnumCollection; + FloatFixedEnum = floatFixedEnum; + FloatFixedEnumWithIntValue = floatFixedEnumWithIntValue; + FloatFixedEnumCollection = floatFixedEnumCollection; + IntFixedEnum = intFixedEnum; + IntFixedEnumCollection = intFixedEnumCollection; + StringFixedEnum = stringFixedEnum; + RequiredUnknown = requiredUnknown; + OptionalUnknown = optionalUnknown; + RequiredRecordUnknown = requiredRecordUnknown; + OptionalRecordUnknown = optionalRecordUnknown; + ReadOnlyRequiredRecordUnknown = readOnlyRequiredRecordUnknown; + ReadOnlyOptionalRecordUnknown = readOnlyOptionalRecordUnknown; + ModelWithRequiredNullable = modelWithRequiredNullable; + RequiredBytes = requiredBytes; + _serializedAdditionalRawData = serializedAdditionalRawData; + } + + /// Initializes a new instance of for deserialization. + internal RoundTripModel() + { + } + + /// The JSON writer. + /// The client options for reading and writing models. void System.ClientModel.Primitives.IJsonModel.Write(System.Text.Json.Utf8JsonWriter writer, System.ClientModel.Primitives.ModelReaderWriterOptions options) { } + /// The JSON reader. + /// The client options for reading and writing models. RoundTripModel System.ClientModel.Primitives.IJsonModel.Create(ref System.Text.Json.Utf8JsonReader reader, System.ClientModel.Primitives.ModelReaderWriterOptions options) { return new RoundTripModel(); } + /// The client options for reading and writing models. System.BinaryData System.ClientModel.Primitives.IPersistableModel.Write(System.ClientModel.Primitives.ModelReaderWriterOptions options) { return new System.BinaryData("IPersistableModel"); } + /// The data to parse. + /// The client options for reading and writing models. RoundTripModel System.ClientModel.Primitives.IPersistableModel.Create(System.BinaryData data, System.ClientModel.Primitives.ModelReaderWriterOptions options) { return new RoundTripModel(); } + /// The client options for reading and writing models. string System.ClientModel.Primitives.IPersistableModel.GetFormatFromOptions(System.ClientModel.Primitives.ModelReaderWriterOptions options) => "J"; } } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/RoundTripModel.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/RoundTripModel.cs index 8e9295bd7b9..3dc41b8df54 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/RoundTripModel.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/RoundTripModel.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using UnbrandedTypeSpec; namespace UnbrandedTypeSpec.Models { @@ -21,113 +22,35 @@ public partial class RoundTripModel /// this is a model with required nullable properties. /// Required bytes. /// , , , , , , or is null. - public RoundTripModel(string requiredString, int requiredInt, IEnumerable requiredCollection, IDictionary requiredDictionary, Thing requiredModel, System.BinaryData requiredUnknown, IDictionary requiredRecordUnknown, ModelWithRequiredNullableProperties modelWithRequiredNullable, System.BinaryData requiredBytes) + public RoundTripModel(string requiredString, int requiredInt, IEnumerable requiredCollection, IDictionary requiredDictionary, Thing requiredModel, System.BinaryData requiredUnknown, IDictionary requiredRecordUnknown, ModelWithRequiredNullableProperties modelWithRequiredNullable, System.BinaryData requiredBytes) { - if (requiredString == null) - { - throw new ArgumentNullException(nameof(requiredString)); - } - if (requiredCollection == null) - { - throw new ArgumentNullException(nameof(requiredCollection)); - } - if (requiredDictionary == null) - { - throw new ArgumentNullException(nameof(requiredDictionary)); - } - if (requiredModel == null) - { - throw new ArgumentNullException(nameof(requiredModel)); - } - if (requiredUnknown == null) - { - throw new ArgumentNullException(nameof(requiredUnknown)); - } - if (requiredRecordUnknown == null) - { - throw new ArgumentNullException(nameof(requiredRecordUnknown)); - } - if (modelWithRequiredNullable == null) - { - throw new ArgumentNullException(nameof(modelWithRequiredNullable)); - } - if (requiredBytes == null) - { - throw new ArgumentNullException(nameof(requiredBytes)); - } + Argument.AssertNotNull(requiredString, nameof(requiredString)); + Argument.AssertNotNull(requiredCollection, nameof(requiredCollection)); + Argument.AssertNotNull(requiredDictionary, nameof(requiredDictionary)); + Argument.AssertNotNull(requiredModel, nameof(requiredModel)); + Argument.AssertNotNull(requiredUnknown, nameof(requiredUnknown)); + Argument.AssertNotNull(requiredRecordUnknown, nameof(requiredRecordUnknown)); + Argument.AssertNotNull(modelWithRequiredNullable, nameof(modelWithRequiredNullable)); + Argument.AssertNotNull(requiredBytes, nameof(requiredBytes)); RequiredString = requiredString; RequiredInt = requiredInt; RequiredCollection = requiredCollection.ToList(); RequiredDictionary = requiredDictionary; RequiredModel = requiredModel; - IntExtensibleEnumCollection = new List(); - FloatExtensibleEnumCollection = new List(); - FloatFixedEnumCollection = new List(); - IntFixedEnumCollection = new List(); + IntExtensibleEnumCollection = new ChangeTrackingList(); + FloatExtensibleEnumCollection = new ChangeTrackingList(); + FloatFixedEnumCollection = new ChangeTrackingList(); + IntFixedEnumCollection = new ChangeTrackingList(); RequiredUnknown = requiredUnknown; RequiredRecordUnknown = requiredRecordUnknown; - OptionalRecordUnknown = new Dictionary(); - ReadOnlyRequiredRecordUnknown = new Dictionary(); - ReadOnlyOptionalRecordUnknown = new Dictionary(); + OptionalRecordUnknown = new ChangeTrackingDictionary(); + ReadOnlyRequiredRecordUnknown = new ChangeTrackingDictionary(); + ReadOnlyOptionalRecordUnknown = new ChangeTrackingDictionary(); ModelWithRequiredNullable = modelWithRequiredNullable; RequiredBytes = requiredBytes; } - /// Initializes a new instance of . - /// Required string, illustrating a reference type property. - /// Required int, illustrating a value type property. - /// Required collection of enums. - /// Required dictionary of enums. - /// Required model. - /// this is an int based extensible enum. - /// this is a collection of int based extensible enum. - /// this is a float based extensible enum. - /// this is a collection of float based extensible enum. - /// this is a float based fixed enum. - /// this is a collection of float based fixed enum. - /// this is a int based fixed enum. - /// this is a collection of int based fixed enum. - /// this is a string based fixed enum. - /// required unknown. - /// optional unknown. - /// required record of unknown. - /// optional record of unknown. - /// required readonly record of unknown. - /// optional readonly record of unknown. - /// this is a model with required nullable properties. - /// Required bytes. - internal RoundTripModel(string requiredString, int requiredInt, IList requiredCollection, IDictionary requiredDictionary, Thing requiredModel, int intExtensibleEnum, IList intExtensibleEnumCollection, float floatExtensibleEnum, IList floatExtensibleEnumCollection, float floatFixedEnum, IList floatFixedEnumCollection, int intFixedEnum, IList intFixedEnumCollection, string stringFixedEnum, System.BinaryData requiredUnknown, System.BinaryData optionalUnknown, IDictionary requiredRecordUnknown, IDictionary optionalRecordUnknown, IDictionary readOnlyRequiredRecordUnknown, IDictionary readOnlyOptionalRecordUnknown, ModelWithRequiredNullableProperties modelWithRequiredNullable, System.BinaryData requiredBytes) - { - RequiredString = requiredString; - RequiredInt = requiredInt; - RequiredCollection = requiredCollection; - RequiredDictionary = requiredDictionary; - RequiredModel = requiredModel; - IntExtensibleEnum = intExtensibleEnum; - IntExtensibleEnumCollection = intExtensibleEnumCollection; - FloatExtensibleEnum = floatExtensibleEnum; - FloatExtensibleEnumCollection = floatExtensibleEnumCollection; - FloatFixedEnum = floatFixedEnum; - FloatFixedEnumCollection = floatFixedEnumCollection; - IntFixedEnum = intFixedEnum; - IntFixedEnumCollection = intFixedEnumCollection; - StringFixedEnum = stringFixedEnum; - RequiredUnknown = requiredUnknown; - OptionalUnknown = optionalUnknown; - RequiredRecordUnknown = requiredRecordUnknown; - OptionalRecordUnknown = optionalRecordUnknown; - ReadOnlyRequiredRecordUnknown = readOnlyRequiredRecordUnknown; - ReadOnlyOptionalRecordUnknown = readOnlyOptionalRecordUnknown; - ModelWithRequiredNullable = modelWithRequiredNullable; - RequiredBytes = requiredBytes; - } - - /// Initializes a new instance of for deserialization. - internal RoundTripModel() - { - } - /// Required string, illustrating a reference type property. public string RequiredString { get; set; } @@ -135,40 +58,46 @@ internal RoundTripModel() public int RequiredInt { get; set; } /// Required collection of enums. - public IList RequiredCollection { get; } + public IList RequiredCollection { get; } /// Required dictionary of enums. - public IDictionary RequiredDictionary { get; } + public IDictionary RequiredDictionary { get; } /// Required model. public Thing RequiredModel { get; set; } /// this is an int based extensible enum. - public int IntExtensibleEnum { get; set; } + public IntExtensibleEnum IntExtensibleEnum { get; set; } /// this is a collection of int based extensible enum. - public IList IntExtensibleEnumCollection { get; } + public IList IntExtensibleEnumCollection { get; } /// this is a float based extensible enum. - public float FloatExtensibleEnum { get; set; } + public FloatExtensibleEnum FloatExtensibleEnum { get; set; } + + /// this is a float based extensible enum. + public FloatExtensibleEnumWithIntValue FloatExtensibleEnumWithIntValue { get; set; } /// this is a collection of float based extensible enum. - public IList FloatExtensibleEnumCollection { get; } + public IList FloatExtensibleEnumCollection { get; } + + /// this is a float based fixed enum. + public FloatFixedEnum FloatFixedEnum { get; set; } /// this is a float based fixed enum. - public float FloatFixedEnum { get; set; } + public FloatFixedEnumWithIntValue FloatFixedEnumWithIntValue { get; set; } /// this is a collection of float based fixed enum. - public IList FloatFixedEnumCollection { get; } + public IList FloatFixedEnumCollection { get; } /// this is a int based fixed enum. - public int IntFixedEnum { get; set; } + public IntFixedEnum IntFixedEnum { get; set; } /// this is a collection of int based fixed enum. - public IList IntFixedEnumCollection { get; } + public IList IntFixedEnumCollection { get; } /// this is a string based fixed enum. - public string StringFixedEnum { get; set; } + public StringFixedEnum? StringFixedEnum { get; set; } /// /// required unknown diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/StringExtensibleEnum.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/StringExtensibleEnum.cs new file mode 100644 index 00000000000..85c7ea524e2 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/StringExtensibleEnum.cs @@ -0,0 +1,62 @@ +// + +#nullable disable + +using System; +using System.ComponentModel; +using UnbrandedTypeSpec; + +namespace UnbrandedTypeSpec.Models +{ + public readonly partial struct StringExtensibleEnum : IEquatable + { + private readonly string _value; + private const string OneValue = "1"; + private const string TwoValue = "2"; + private const string FourValue = "4"; + + /// Initializes a new instance of . + /// The value. + /// is null. + public StringExtensibleEnum(string value) + { + Argument.AssertNotNull(value, nameof(value)); + + _value = value; + } + + /// Gets the one. + public static StringExtensibleEnum One { get; } = new StringExtensibleEnum(OneValue); + + /// Gets the two. + public static StringExtensibleEnum Two { get; } = new StringExtensibleEnum(TwoValue); + + /// Gets the four. + public static StringExtensibleEnum Four { get; } = new StringExtensibleEnum(FourValue); + + /// Determines if two values are the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator ==(StringExtensibleEnum left, StringExtensibleEnum right) => left.Equals(right); + + /// Determines if two values are not the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator !=(StringExtensibleEnum left, StringExtensibleEnum right) => !left.Equals(right); + + /// Converts a string to a . + /// The value. + public static implicit operator StringExtensibleEnum(string value) => new StringExtensibleEnum(value); + + /// The object to compare. + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => obj is StringExtensibleEnum other && Equals(other); + + /// The instance to compare. + public bool Equals(StringExtensibleEnum other) => string.Equals(_value, other._value, StringComparison.InvariantCultureIgnoreCase); + + public override int GetHashCode() => _value?.GetHashCode() ?? 0; + + public override string ToString() => _value; + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/StringFixedEnum.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/StringFixedEnum.cs new file mode 100644 index 00000000000..50aaf6c30e6 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/StringFixedEnum.cs @@ -0,0 +1,13 @@ +// + +#nullable disable + +namespace UnbrandedTypeSpec.Models +{ + public enum StringFixedEnum + { + One, + Two, + Four + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/StringFixedEnumExtensions.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/StringFixedEnumExtensions.cs new file mode 100644 index 00000000000..149721ff2de --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/StringFixedEnumExtensions.cs @@ -0,0 +1,38 @@ +// + +#nullable disable + +using System; + +namespace UnbrandedTypeSpec.Models +{ + internal static partial class StringFixedEnumExtensions + { + /// The value to serialize. + public static string ToSerialString(this StringFixedEnum value) => value switch + { + StringFixedEnum.One => "1", + StringFixedEnum.Two => "2", + StringFixedEnum.Four => "4", + _ => throw new ArgumentOutOfRangeException(nameof(value), value, "Unknown StringFixedEnum value.") + }; + + /// The value to deserialize. + public static StringFixedEnum ToStringFixedEnum(this string value) + { + if (StringComparer.OrdinalIgnoreCase.Equals(value, "1")) + { + return StringFixedEnum.One; + } + if (StringComparer.OrdinalIgnoreCase.Equals(value, "2")) + { + return StringFixedEnum.Two; + } + if (StringComparer.OrdinalIgnoreCase.Equals(value, "4")) + { + return StringFixedEnum.Four; + } + throw new ArgumentOutOfRangeException(nameof(value), value, "Unknown StringFixedEnum value."); + } + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/Thing.Serialization.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/Thing.Serialization.cs index e405ed26cd4..2b15ac6b981 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/Thing.Serialization.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/Thing.Serialization.cs @@ -4,31 +4,80 @@ using System; using System.ClientModel.Primitives; +using System.Collections.Generic; using System.Text.Json; namespace UnbrandedTypeSpec.Models { public partial class Thing : System.ClientModel.Primitives.IJsonModel { + private IDictionary _serializedAdditionalRawData; + + /// Initializes a new instance of . + /// name of the Thing. + /// required Union. + /// required literal string. + /// required literal int. + /// required literal float. + /// required literal bool. + /// optional literal string. + /// optional literal int. + /// optional literal float. + /// optional literal bool. + /// description with xml <|endoftext|>. + /// optional nullable collection. + /// required nullable collection. + /// Keeps track of any properties unknown to the library. + internal Thing(string name, System.BinaryData requiredUnion, ThingRequiredLiteralString requiredLiteralString, ThingRequiredLiteralInt requiredLiteralInt, ThingRequiredLiteralFloat requiredLiteralFloat, bool requiredLiteralBool, ThingOptionalLiteralString optionalLiteralString, ThingOptionalLiteralInt optionalLiteralInt, ThingOptionalLiteralFloat optionalLiteralFloat, bool optionalLiteralBool, string requiredBadDescription, IList optionalNullableList, IList requiredNullableList, IDictionary serializedAdditionalRawData) + { + Name = name; + RequiredUnion = requiredUnion; + RequiredLiteralString = requiredLiteralString; + RequiredLiteralInt = requiredLiteralInt; + RequiredLiteralFloat = requiredLiteralFloat; + RequiredLiteralBool = requiredLiteralBool; + OptionalLiteralString = optionalLiteralString; + OptionalLiteralInt = optionalLiteralInt; + OptionalLiteralFloat = optionalLiteralFloat; + OptionalLiteralBool = optionalLiteralBool; + RequiredBadDescription = requiredBadDescription; + OptionalNullableList = optionalNullableList; + RequiredNullableList = requiredNullableList; + _serializedAdditionalRawData = serializedAdditionalRawData; + } + + /// Initializes a new instance of for deserialization. + internal Thing() + { + } + + /// The JSON writer. + /// The client options for reading and writing models. void System.ClientModel.Primitives.IJsonModel.Write(System.Text.Json.Utf8JsonWriter writer, System.ClientModel.Primitives.ModelReaderWriterOptions options) { } + /// The JSON reader. + /// The client options for reading and writing models. Thing System.ClientModel.Primitives.IJsonModel.Create(ref System.Text.Json.Utf8JsonReader reader, System.ClientModel.Primitives.ModelReaderWriterOptions options) { return new Thing(); } + /// The client options for reading and writing models. System.BinaryData System.ClientModel.Primitives.IPersistableModel.Write(System.ClientModel.Primitives.ModelReaderWriterOptions options) { return new System.BinaryData("IPersistableModel"); } + /// The data to parse. + /// The client options for reading and writing models. Thing System.ClientModel.Primitives.IPersistableModel.Create(System.BinaryData data, System.ClientModel.Primitives.ModelReaderWriterOptions options) { return new Thing(); } + /// The client options for reading and writing models. string System.ClientModel.Primitives.IPersistableModel.GetFormatFromOptions(System.ClientModel.Primitives.ModelReaderWriterOptions options) => "J"; } } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/Thing.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/Thing.cs index 98cae1ff105..b1ff404e7bf 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/Thing.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/Thing.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using UnbrandedTypeSpec; namespace UnbrandedTypeSpec.Models { @@ -18,62 +19,17 @@ public partial class Thing /// , or is null. public Thing(string name, System.BinaryData requiredUnion, string requiredBadDescription, IEnumerable requiredNullableList) { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - if (requiredUnion == null) - { - throw new ArgumentNullException(nameof(requiredUnion)); - } - if (requiredBadDescription == null) - { - throw new ArgumentNullException(nameof(requiredBadDescription)); - } + Argument.AssertNotNull(name, nameof(name)); + Argument.AssertNotNull(requiredUnion, nameof(requiredUnion)); + Argument.AssertNotNull(requiredBadDescription, nameof(requiredBadDescription)); Name = name; RequiredUnion = requiredUnion; RequiredBadDescription = requiredBadDescription; - OptionalNullableList = new List(); + OptionalNullableList = new ChangeTrackingList(); RequiredNullableList = requiredNullableList?.ToList(); } - /// Initializes a new instance of . - /// name of the Thing. - /// required Union. - /// required literal string. - /// required literal int. - /// required literal float. - /// required literal bool. - /// optional literal string. - /// optional literal int. - /// optional literal float. - /// optional literal bool. - /// description with xml <|endoftext|>. - /// optional nullable collection. - /// required nullable collection. - internal Thing(string name, System.BinaryData requiredUnion, string requiredLiteralString, int requiredLiteralInt, float requiredLiteralFloat, bool requiredLiteralBool, string optionalLiteralString, int optionalLiteralInt, float optionalLiteralFloat, bool optionalLiteralBool, string requiredBadDescription, IList optionalNullableList, IList requiredNullableList) - { - Name = name; - RequiredUnion = requiredUnion; - RequiredLiteralString = requiredLiteralString; - RequiredLiteralInt = requiredLiteralInt; - RequiredLiteralFloat = requiredLiteralFloat; - RequiredLiteralBool = requiredLiteralBool; - OptionalLiteralString = optionalLiteralString; - OptionalLiteralInt = optionalLiteralInt; - OptionalLiteralFloat = optionalLiteralFloat; - OptionalLiteralBool = optionalLiteralBool; - RequiredBadDescription = requiredBadDescription; - OptionalNullableList = optionalNullableList; - RequiredNullableList = requiredNullableList; - } - - /// Initializes a new instance of for deserialization. - internal Thing() - { - } - /// name of the Thing. public string Name { get; set; } @@ -124,25 +80,25 @@ internal Thing() public System.BinaryData RequiredUnion { get; set; } /// required literal string. - public string RequiredLiteralString { get; } = "accept"; + public ThingRequiredLiteralString RequiredLiteralString { get; } = "accept"; /// required literal int. - public int RequiredLiteralInt { get; } = 123; + public ThingRequiredLiteralInt RequiredLiteralInt { get; } = 123; /// required literal float. - public float RequiredLiteralFloat { get; } = 1.23F; + public ThingRequiredLiteralFloat RequiredLiteralFloat { get; } = 1.23F; /// required literal bool. public bool RequiredLiteralBool { get; } = false; /// optional literal string. - public string OptionalLiteralString { get; set; } + public ThingOptionalLiteralString OptionalLiteralString { get; set; } /// optional literal int. - public int OptionalLiteralInt { get; set; } + public ThingOptionalLiteralInt OptionalLiteralInt { get; set; } /// optional literal float. - public float OptionalLiteralFloat { get; set; } + public ThingOptionalLiteralFloat OptionalLiteralFloat { get; set; } /// optional literal bool. public bool OptionalLiteralBool { get; set; } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ThingOptionalLiteralFloat.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ThingOptionalLiteralFloat.cs new file mode 100644 index 00000000000..0bfb22fe69c --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ThingOptionalLiteralFloat.cs @@ -0,0 +1,54 @@ +// + +#nullable disable + +using System; +using System.ComponentModel; +using System.Globalization; + +namespace UnbrandedTypeSpec.Models +{ + public readonly partial struct ThingOptionalLiteralFloat : IEquatable + { + private readonly float _value; + /// 4.56. + private const float _456Value = 4.56F; + + /// Initializes a new instance of . + /// The value. + public ThingOptionalLiteralFloat(float value) + { + _value = value; + } + + /// 4.56. + public static ThingOptionalLiteralFloat _456 { get; } = new ThingOptionalLiteralFloat(_456Value); + + /// Determines if two values are the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator ==(ThingOptionalLiteralFloat left, ThingOptionalLiteralFloat right) => left.Equals(right); + + /// Determines if two values are not the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator !=(ThingOptionalLiteralFloat left, ThingOptionalLiteralFloat right) => !left.Equals(right); + + /// Converts a string to a . + /// The value. + public static implicit operator ThingOptionalLiteralFloat(float value) => new ThingOptionalLiteralFloat(value); + + /// The object to compare. + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => obj is ThingOptionalLiteralFloat other && Equals(other); + + /// The instance to compare. + public bool Equals(ThingOptionalLiteralFloat other) => Equals(_value, other._value); + + public override int GetHashCode() => _value.GetHashCode(); + + public override string ToString() => _value.ToString(CultureInfo.InvariantCulture); + + internal float ToSerialSingle() => _value; + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ThingOptionalLiteralInt.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ThingOptionalLiteralInt.cs new file mode 100644 index 00000000000..f9524c0106f --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ThingOptionalLiteralInt.cs @@ -0,0 +1,54 @@ +// + +#nullable disable + +using System; +using System.ComponentModel; +using System.Globalization; + +namespace UnbrandedTypeSpec.Models +{ + public readonly partial struct ThingOptionalLiteralInt : IEquatable + { + private readonly int _value; + /// 456. + private const int _456Value = 456; + + /// Initializes a new instance of . + /// The value. + public ThingOptionalLiteralInt(int value) + { + _value = value; + } + + /// 456. + public static ThingOptionalLiteralInt _456 { get; } = new ThingOptionalLiteralInt(_456Value); + + /// Determines if two values are the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator ==(ThingOptionalLiteralInt left, ThingOptionalLiteralInt right) => left.Equals(right); + + /// Determines if two values are not the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator !=(ThingOptionalLiteralInt left, ThingOptionalLiteralInt right) => !left.Equals(right); + + /// Converts a string to a . + /// The value. + public static implicit operator ThingOptionalLiteralInt(int value) => new ThingOptionalLiteralInt(value); + + /// The object to compare. + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => obj is ThingOptionalLiteralInt other && Equals(other); + + /// The instance to compare. + public bool Equals(ThingOptionalLiteralInt other) => Equals(_value, other._value); + + public override int GetHashCode() => _value.GetHashCode(); + + public override string ToString() => _value.ToString(CultureInfo.InvariantCulture); + + internal int ToSerialInt32() => _value; + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ThingOptionalLiteralString.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ThingOptionalLiteralString.cs new file mode 100644 index 00000000000..5f868c640df --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ThingOptionalLiteralString.cs @@ -0,0 +1,55 @@ +// + +#nullable disable + +using System; +using System.ComponentModel; +using UnbrandedTypeSpec; + +namespace UnbrandedTypeSpec.Models +{ + public readonly partial struct ThingOptionalLiteralString : IEquatable + { + private readonly string _value; + /// reject. + private const string RejectValue = "reject"; + + /// Initializes a new instance of . + /// The value. + /// is null. + public ThingOptionalLiteralString(string value) + { + Argument.AssertNotNull(value, nameof(value)); + + _value = value; + } + + /// reject. + public static ThingOptionalLiteralString Reject { get; } = new ThingOptionalLiteralString(RejectValue); + + /// Determines if two values are the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator ==(ThingOptionalLiteralString left, ThingOptionalLiteralString right) => left.Equals(right); + + /// Determines if two values are not the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator !=(ThingOptionalLiteralString left, ThingOptionalLiteralString right) => !left.Equals(right); + + /// Converts a string to a . + /// The value. + public static implicit operator ThingOptionalLiteralString(string value) => new ThingOptionalLiteralString(value); + + /// The object to compare. + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => obj is ThingOptionalLiteralString other && Equals(other); + + /// The instance to compare. + public bool Equals(ThingOptionalLiteralString other) => string.Equals(_value, other._value, StringComparison.InvariantCultureIgnoreCase); + + public override int GetHashCode() => _value?.GetHashCode() ?? 0; + + public override string ToString() => _value; + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ThingRequiredLiteralFloat.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ThingRequiredLiteralFloat.cs new file mode 100644 index 00000000000..bc25084e49f --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ThingRequiredLiteralFloat.cs @@ -0,0 +1,54 @@ +// + +#nullable disable + +using System; +using System.ComponentModel; +using System.Globalization; + +namespace UnbrandedTypeSpec.Models +{ + public readonly partial struct ThingRequiredLiteralFloat : IEquatable + { + private readonly float _value; + /// 1.23. + private const float _123Value = 1.23F; + + /// Initializes a new instance of . + /// The value. + public ThingRequiredLiteralFloat(float value) + { + _value = value; + } + + /// 1.23. + public static ThingRequiredLiteralFloat _123 { get; } = new ThingRequiredLiteralFloat(_123Value); + + /// Determines if two values are the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator ==(ThingRequiredLiteralFloat left, ThingRequiredLiteralFloat right) => left.Equals(right); + + /// Determines if two values are not the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator !=(ThingRequiredLiteralFloat left, ThingRequiredLiteralFloat right) => !left.Equals(right); + + /// Converts a string to a . + /// The value. + public static implicit operator ThingRequiredLiteralFloat(float value) => new ThingRequiredLiteralFloat(value); + + /// The object to compare. + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => obj is ThingRequiredLiteralFloat other && Equals(other); + + /// The instance to compare. + public bool Equals(ThingRequiredLiteralFloat other) => Equals(_value, other._value); + + public override int GetHashCode() => _value.GetHashCode(); + + public override string ToString() => _value.ToString(CultureInfo.InvariantCulture); + + internal float ToSerialSingle() => _value; + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ThingRequiredLiteralInt.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ThingRequiredLiteralInt.cs new file mode 100644 index 00000000000..819c5f879db --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ThingRequiredLiteralInt.cs @@ -0,0 +1,54 @@ +// + +#nullable disable + +using System; +using System.ComponentModel; +using System.Globalization; + +namespace UnbrandedTypeSpec.Models +{ + public readonly partial struct ThingRequiredLiteralInt : IEquatable + { + private readonly int _value; + /// 123. + private const int _123Value = 123; + + /// Initializes a new instance of . + /// The value. + public ThingRequiredLiteralInt(int value) + { + _value = value; + } + + /// 123. + public static ThingRequiredLiteralInt _123 { get; } = new ThingRequiredLiteralInt(_123Value); + + /// Determines if two values are the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator ==(ThingRequiredLiteralInt left, ThingRequiredLiteralInt right) => left.Equals(right); + + /// Determines if two values are not the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator !=(ThingRequiredLiteralInt left, ThingRequiredLiteralInt right) => !left.Equals(right); + + /// Converts a string to a . + /// The value. + public static implicit operator ThingRequiredLiteralInt(int value) => new ThingRequiredLiteralInt(value); + + /// The object to compare. + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => obj is ThingRequiredLiteralInt other && Equals(other); + + /// The instance to compare. + public bool Equals(ThingRequiredLiteralInt other) => Equals(_value, other._value); + + public override int GetHashCode() => _value.GetHashCode(); + + public override string ToString() => _value.ToString(CultureInfo.InvariantCulture); + + internal int ToSerialInt32() => _value; + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ThingRequiredLiteralString.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ThingRequiredLiteralString.cs new file mode 100644 index 00000000000..62a46a90d61 --- /dev/null +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/Models/ThingRequiredLiteralString.cs @@ -0,0 +1,55 @@ +// + +#nullable disable + +using System; +using System.ComponentModel; +using UnbrandedTypeSpec; + +namespace UnbrandedTypeSpec.Models +{ + public readonly partial struct ThingRequiredLiteralString : IEquatable + { + private readonly string _value; + /// accept. + private const string AcceptValue = "accept"; + + /// Initializes a new instance of . + /// The value. + /// is null. + public ThingRequiredLiteralString(string value) + { + Argument.AssertNotNull(value, nameof(value)); + + _value = value; + } + + /// accept. + public static ThingRequiredLiteralString Accept { get; } = new ThingRequiredLiteralString(AcceptValue); + + /// Determines if two values are the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator ==(ThingRequiredLiteralString left, ThingRequiredLiteralString right) => left.Equals(right); + + /// Determines if two values are not the same. + /// The left value to compare. + /// The right value to compare. + public static bool operator !=(ThingRequiredLiteralString left, ThingRequiredLiteralString right) => !left.Equals(right); + + /// Converts a string to a . + /// The value. + public static implicit operator ThingRequiredLiteralString(string value) => new ThingRequiredLiteralString(value); + + /// The object to compare. + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => obj is ThingRequiredLiteralString other && Equals(other); + + /// The instance to compare. + public bool Equals(ThingRequiredLiteralString other) => string.Equals(_value, other._value, StringComparison.InvariantCultureIgnoreCase); + + public override int GetHashCode() => _value?.GetHashCode() ?? 0; + + public override string ToString() => _value; + } +} diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/UnbrandedTypeSpecClient.cs b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/UnbrandedTypeSpecClient.cs index d4d23f3222d..93fb6a361ff 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/UnbrandedTypeSpecClient.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/src/Generated/UnbrandedTypeSpecClient.cs @@ -129,17 +129,6 @@ internal void CreateAddTimeHeaderRequest(System.Uri unbrandedTypeSpecUrl, DateTi { } - /// parameter has string format. - /// - /// - /// - /// - /// - /// , , , or is null. - internal void CreateStringFormatRequest(System.Uri unbrandedTypeSpecUrl, Guid subscriptionId, ModelWithFormat body, string contentType, string accept) - { - } - /// Model can have its projected name. /// /// this is a model with a projected name. diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/tspCodeModel.json b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/tspCodeModel.json index 71916b22e6b..c0caf4b6c85 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/tspCodeModel.json +++ b/packages/http-client-csharp/generator/TestProjects/Local/Unbranded-TypeSpec/tspCodeModel.json @@ -1,17 +1,20 @@ { "$id": "1", "Name": "UnbrandedTypeSpec", - "Description": "This is a sample typespec project.", "ApiVersions": [], "Enums": [ { "$id": "2", - "Kind": "Enum", + "Kind": "enum", "Name": "Thing_requiredLiteralString", - "EnumValueType": "String", - "AllowedValues": [ + "ValueType": { + "$id": "3", + "Kind": "string", + "IsNullable": false + }, + "Values": [ { - "$id": "3", + "$id": "4", "Name": "accept", "Value": "accept", "Description": "accept" @@ -24,13 +27,17 @@ "Usage": "RoundTrip" }, { - "$id": "4", - "Kind": "Enum", + "$id": "5", + "Kind": "enum", "Name": "Thing_requiredLiteralInt", - "EnumValueType": "Int32", - "AllowedValues": [ + "ValueType": { + "$id": "6", + "Kind": "int32", + "IsNullable": false + }, + "Values": [ { - "$id": "5", + "$id": "7", "Name": "123", "Value": 123, "Description": "123" @@ -43,13 +50,17 @@ "Usage": "RoundTrip" }, { - "$id": "6", - "Kind": "Enum", + "$id": "8", + "Kind": "enum", "Name": "Thing_requiredLiteralFloat", - "EnumValueType": "Float32", - "AllowedValues": [ + "ValueType": { + "$id": "9", + "Kind": "float32", + "IsNullable": false + }, + "Values": [ { - "$id": "7", + "$id": "10", "Name": "1.23", "Value": 1.23, "Description": "1.23" @@ -62,13 +73,17 @@ "Usage": "RoundTrip" }, { - "$id": "8", - "Kind": "Enum", + "$id": "11", + "Kind": "enum", "Name": "Thing_optionalLiteralString", - "EnumValueType": "String", - "AllowedValues": [ + "ValueType": { + "$id": "12", + "Kind": "string", + "IsNullable": false + }, + "Values": [ { - "$id": "9", + "$id": "13", "Name": "reject", "Value": "reject", "Description": "reject" @@ -81,13 +96,17 @@ "Usage": "RoundTrip" }, { - "$id": "10", - "Kind": "Enum", + "$id": "14", + "Kind": "enum", "Name": "Thing_optionalLiteralInt", - "EnumValueType": "Int32", - "AllowedValues": [ + "ValueType": { + "$id": "15", + "Kind": "int32", + "IsNullable": false + }, + "Values": [ { - "$id": "11", + "$id": "16", "Name": "456", "Value": 456, "Description": "456" @@ -100,13 +119,17 @@ "Usage": "RoundTrip" }, { - "$id": "12", - "Kind": "Enum", + "$id": "17", + "Kind": "enum", "Name": "Thing_optionalLiteralFloat", - "EnumValueType": "Float32", - "AllowedValues": [ + "ValueType": { + "$id": "18", + "Kind": "float32", + "IsNullable": false + }, + "Values": [ { - "$id": "13", + "$id": "19", "Name": "4.56", "Value": 4.56, "Description": "4.56" @@ -119,23 +142,27 @@ "Usage": "RoundTrip" }, { - "$id": "14", - "Kind": "Enum", + "$id": "20", + "Kind": "enum", "Name": "StringFixedEnum", - "EnumValueType": "String", - "AllowedValues": [ + "ValueType": { + "$id": "21", + "Kind": "string", + "IsNullable": false + }, + "Values": [ { - "$id": "15", + "$id": "22", "Name": "One", "Value": "1" }, { - "$id": "16", + "$id": "23", "Name": "Two", "Value": "2" }, { - "$id": "17", + "$id": "24", "Name": "Four", "Value": "4" } @@ -147,23 +174,27 @@ "Usage": "RoundTrip" }, { - "$id": "18", - "Kind": "Enum", + "$id": "25", + "Kind": "enum", "Name": "StringExtensibleEnum", - "EnumValueType": "String", - "AllowedValues": [ + "ValueType": { + "$id": "26", + "Kind": "string", + "IsNullable": false + }, + "Values": [ { - "$id": "19", + "$id": "27", "Name": "One", "Value": "1" }, { - "$id": "20", + "$id": "28", "Name": "Two", "Value": "2" }, { - "$id": "21", + "$id": "29", "Name": "Four", "Value": "4" } @@ -175,23 +206,27 @@ "Usage": "RoundTrip" }, { - "$id": "22", - "Kind": "Enum", + "$id": "30", + "Kind": "enum", "Name": "IntExtensibleEnum", - "EnumValueType": "Int32", - "AllowedValues": [ + "ValueType": { + "$id": "31", + "Kind": "int32", + "IsNullable": false + }, + "Values": [ { - "$id": "23", + "$id": "32", "Name": "One", "Value": 1 }, { - "$id": "24", + "$id": "33", "Name": "Two", "Value": 2 }, { - "$id": "25", + "$id": "34", "Name": "Four", "Value": 4 } @@ -203,52 +238,92 @@ "Usage": "RoundTrip" }, { - "$id": "26", - "Kind": "Enum", + "$id": "35", + "Kind": "enum", "Name": "FloatExtensibleEnum", - "EnumValueType": "Float32", - "AllowedValues": [ + "ValueType": { + "$id": "36", + "Kind": "float32", + "IsNullable": false + }, + "Values": [ { - "$id": "27", + "$id": "37", + "Name": "OneDotOne", + "Value": 1.1 + }, + { + "$id": "38", + "Name": "TwoDotTwo", + "Value": 2.2 + }, + { + "$id": "39", + "Name": "FourDotFour", + "Value": 4.4 + } + ], + "Namespace": "UnbrandedTypeSpec", + "Description": "Float based extensible enum", + "IsExtensible": true, + "IsNullable": false, + "Usage": "RoundTrip" + }, + { + "$id": "40", + "Kind": "enum", + "Name": "FloatExtensibleEnumWithIntValue", + "ValueType": { + "$id": "41", + "Kind": "float32", + "IsNullable": false + }, + "Values": [ + { + "$id": "42", "Name": "One", "Value": 1 }, { - "$id": "28", + "$id": "43", "Name": "Two", "Value": 2 }, { - "$id": "29", + "$id": "44", "Name": "Four", "Value": 4 } ], "Namespace": "UnbrandedTypeSpec", - "Description": "Float based extensible enum", + "Description": "float fixed enum", "IsExtensible": true, "IsNullable": false, "Usage": "RoundTrip" }, { - "$id": "30", - "Kind": "Enum", + "$id": "45", + "Kind": "enum", "Name": "FloatFixedEnum", - "EnumValueType": "Float32", - "AllowedValues": [ + "ValueType": { + "$id": "46", + "Kind": "float32", + "IsNullable": false + }, + "Values": [ { - "$id": "31", - "Name": "One", + "$id": "47", + "Name": "OneDotOne", "Value": 1.1 }, { - "$id": "32", - "Name": "Two", + "$id": "48", + "Name": "TwoDotTwo", "Value": 2.2 }, { - "$id": "33", - "Name": "Four", + "$id": "49", + "Name": "FourDotFour", "Value": 4.4 } ], @@ -259,23 +334,59 @@ "Usage": "RoundTrip" }, { - "$id": "34", - "Kind": "Enum", + "$id": "50", + "Kind": "enum", + "Name": "FloatFixedEnumWithIntValue", + "ValueType": { + "$id": "51", + "Kind": "int32", + "IsNullable": false + }, + "Values": [ + { + "$id": "52", + "Name": "One", + "Value": 1 + }, + { + "$id": "53", + "Name": "Two", + "Value": 2 + }, + { + "$id": "54", + "Name": "Four", + "Value": 4 + } + ], + "Namespace": "UnbrandedTypeSpec", + "Description": "float fixed enum", + "IsExtensible": false, + "IsNullable": false, + "Usage": "RoundTrip" + }, + { + "$id": "55", + "Kind": "enum", "Name": "IntFixedEnum", - "EnumValueType": "Int32", - "AllowedValues": [ + "ValueType": { + "$id": "56", + "Kind": "int32", + "IsNullable": false + }, + "Values": [ { - "$id": "35", + "$id": "57", "Name": "One", "Value": 1 }, { - "$id": "36", + "$id": "58", "Name": "Two", "Value": 2 }, { - "$id": "37", + "$id": "59", "Name": "Four", "Value": 4 } @@ -289,7 +400,7 @@ ], "Models": [ { - "$id": "38", + "$id": "60", "Kind": "Model", "Name": "Thing", "Namespace": "UnbrandedTypeSpec", @@ -298,51 +409,47 @@ "Usage": "RoundTrip", "Properties": [ { - "$id": "39", + "$id": "61", "Name": "name", "SerializedName": "name", "Description": "name of the Thing", "Type": { - "$id": "40", - "Kind": "Primitive", - "Name": "String", + "$id": "62", + "Kind": "string", "IsNullable": false }, "IsRequired": true, "IsReadOnly": false }, { - "$id": "41", + "$id": "63", "Name": "requiredUnion", "SerializedName": "requiredUnion", "Description": "required Union", "Type": { - "$id": "42", - "Kind": "Union", - "Name": "Union", - "UnionItemTypes": [ + "$id": "64", + "Kind": "union", + "Name": "ThingRequiredUnion", + "VariantTypes": [ { - "$id": "43", - "Kind": "Primitive", - "Name": "String", + "$id": "65", + "Kind": "string", "IsNullable": false }, { - "$id": "44", + "$id": "66", "Kind": "Array", "Name": "Array", "ElementType": { - "$id": "45", - "Kind": "Primitive", - "Name": "String", + "$id": "67", + "Kind": "string", "IsNullable": false }, "IsNullable": false }, { - "$id": "46", - "Kind": "Primitive", - "Name": "Int32", + "$id": "68", + "Kind": "int32", "IsNullable": false } ], @@ -352,15 +459,14 @@ "IsReadOnly": false }, { - "$id": "47", + "$id": "69", "Name": "requiredLiteralString", "SerializedName": "requiredLiteralString", "Description": "required literal string", "Type": { - "$id": "48", - "Kind": "Literal", - "Name": "Literal", - "LiteralValueType": { + "$id": "70", + "Kind": "constant", + "ValueType": { "$ref": "2" }, "Value": "accept", @@ -370,16 +476,15 @@ "IsReadOnly": false }, { - "$id": "49", + "$id": "71", "Name": "requiredLiteralInt", "SerializedName": "requiredLiteralInt", "Description": "required literal int", "Type": { - "$id": "50", - "Kind": "Literal", - "Name": "Literal", - "LiteralValueType": { - "$ref": "4" + "$id": "72", + "Kind": "constant", + "ValueType": { + "$ref": "5" }, "Value": 123, "IsNullable": false @@ -388,16 +493,15 @@ "IsReadOnly": false }, { - "$id": "51", + "$id": "73", "Name": "requiredLiteralFloat", "SerializedName": "requiredLiteralFloat", "Description": "required literal float", "Type": { - "$id": "52", - "Kind": "Literal", - "Name": "Literal", - "LiteralValueType": { - "$ref": "6" + "$id": "74", + "Kind": "constant", + "ValueType": { + "$ref": "8" }, "Value": 1.23, "IsNullable": false @@ -406,18 +510,16 @@ "IsReadOnly": false }, { - "$id": "53", + "$id": "75", "Name": "requiredLiteralBool", "SerializedName": "requiredLiteralBool", "Description": "required literal bool", "Type": { - "$id": "54", - "Kind": "Literal", - "Name": "Literal", - "LiteralValueType": { - "$id": "55", - "Kind": "Primitive", - "Name": "Boolean", + "$id": "76", + "Kind": "constant", + "ValueType": { + "$id": "77", + "Kind": "boolean", "IsNullable": false }, "Value": false, @@ -427,16 +529,15 @@ "IsReadOnly": false }, { - "$id": "56", + "$id": "78", "Name": "optionalLiteralString", "SerializedName": "optionalLiteralString", "Description": "optional literal string", "Type": { - "$id": "57", - "Kind": "Literal", - "Name": "Literal", - "LiteralValueType": { - "$ref": "8" + "$id": "79", + "Kind": "constant", + "ValueType": { + "$ref": "11" }, "Value": "reject", "IsNullable": false @@ -445,16 +546,15 @@ "IsReadOnly": false }, { - "$id": "58", + "$id": "80", "Name": "optionalLiteralInt", "SerializedName": "optionalLiteralInt", "Description": "optional literal int", "Type": { - "$id": "59", - "Kind": "Literal", - "Name": "Literal", - "LiteralValueType": { - "$ref": "10" + "$id": "81", + "Kind": "constant", + "ValueType": { + "$ref": "14" }, "Value": 456, "IsNullable": false @@ -463,16 +563,15 @@ "IsReadOnly": false }, { - "$id": "60", + "$id": "82", "Name": "optionalLiteralFloat", "SerializedName": "optionalLiteralFloat", "Description": "optional literal float", "Type": { - "$id": "61", - "Kind": "Literal", - "Name": "Literal", - "LiteralValueType": { - "$ref": "12" + "$id": "83", + "Kind": "constant", + "ValueType": { + "$ref": "17" }, "Value": 4.56, "IsNullable": false @@ -481,18 +580,16 @@ "IsReadOnly": false }, { - "$id": "62", + "$id": "84", "Name": "optionalLiteralBool", "SerializedName": "optionalLiteralBool", "Description": "optional literal bool", "Type": { - "$id": "63", - "Kind": "Literal", - "Name": "Literal", - "LiteralValueType": { - "$id": "64", - "Kind": "Primitive", - "Name": "Boolean", + "$id": "85", + "Kind": "constant", + "ValueType": { + "$id": "86", + "Kind": "boolean", "IsNullable": false }, "Value": true, @@ -502,32 +599,30 @@ "IsReadOnly": false }, { - "$id": "65", + "$id": "87", "Name": "requiredBadDescription", "SerializedName": "requiredBadDescription", "Description": "description with xml <|endoftext|>", "Type": { - "$id": "66", - "Kind": "Primitive", - "Name": "String", + "$id": "88", + "Kind": "string", "IsNullable": false }, "IsRequired": true, "IsReadOnly": false }, { - "$id": "67", + "$id": "89", "Name": "optionalNullableList", "SerializedName": "optionalNullableList", "Description": "optional nullable collection", "Type": { - "$id": "68", + "$id": "90", "Kind": "Array", "Name": "Array", "ElementType": { - "$id": "69", - "Kind": "Primitive", - "Name": "Int32", + "$id": "91", + "Kind": "int32", "IsNullable": false }, "IsNullable": true @@ -536,18 +631,17 @@ "IsReadOnly": false }, { - "$id": "70", + "$id": "92", "Name": "requiredNullableList", "SerializedName": "requiredNullableList", "Description": "required nullable collection", "Type": { - "$id": "71", + "$id": "93", "Kind": "Array", "Name": "Array", "ElementType": { - "$id": "72", - "Kind": "Primitive", - "Name": "Int32", + "$id": "94", + "Kind": "int32", "IsNullable": false }, "IsNullable": true @@ -558,7 +652,7 @@ ] }, { - "$id": "73", + "$id": "95", "Kind": "Model", "Name": "RoundTripModel", "Namespace": "UnbrandedTypeSpec", @@ -567,44 +661,42 @@ "Usage": "RoundTrip", "Properties": [ { - "$id": "74", + "$id": "96", "Name": "requiredString", "SerializedName": "requiredString", "Description": "Required string, illustrating a reference type property.", "Type": { - "$id": "75", - "Kind": "Primitive", - "Name": "String", + "$id": "97", + "Kind": "string", "IsNullable": false }, "IsRequired": true, "IsReadOnly": false }, { - "$id": "76", + "$id": "98", "Name": "requiredInt", "SerializedName": "requiredInt", "Description": "Required int, illustrating a value type property.", "Type": { - "$id": "77", - "Kind": "Primitive", - "Name": "Int32", + "$id": "99", + "Kind": "int32", "IsNullable": false }, "IsRequired": true, "IsReadOnly": false }, { - "$id": "78", + "$id": "100", "Name": "requiredCollection", "SerializedName": "requiredCollection", "Description": "Required collection of enums", "Type": { - "$id": "79", + "$id": "101", "Kind": "Array", "Name": "Array", "ElementType": { - "$ref": "14" + "$ref": "20" }, "IsNullable": false }, @@ -612,22 +704,21 @@ "IsReadOnly": false }, { - "$id": "80", + "$id": "102", "Name": "requiredDictionary", "SerializedName": "requiredDictionary", "Description": "Required dictionary of enums", "Type": { - "$id": "81", + "$id": "103", "Kind": "Dictionary", "Name": "Dictionary", "KeyType": { - "$id": "82", - "Kind": "Primitive", - "Name": "String", + "$id": "104", + "Kind": "string", "IsNullable": false }, "ValueType": { - "$ref": "18" + "$ref": "25" }, "IsNullable": false }, @@ -635,38 +726,38 @@ "IsReadOnly": false }, { - "$id": "83", + "$id": "105", "Name": "requiredModel", "SerializedName": "requiredModel", "Description": "Required model", "Type": { - "$ref": "38" + "$ref": "60" }, "IsRequired": true, "IsReadOnly": false }, { - "$id": "84", + "$id": "106", "Name": "intExtensibleEnum", "SerializedName": "intExtensibleEnum", "Description": "this is an int based extensible enum", "Type": { - "$ref": "22" + "$ref": "30" }, "IsRequired": false, "IsReadOnly": false }, { - "$id": "85", + "$id": "107", "Name": "intExtensibleEnumCollection", "SerializedName": "intExtensibleEnumCollection", "Description": "this is a collection of int based extensible enum", "Type": { - "$id": "86", + "$id": "108", "Kind": "Array", "Name": "Array", "ElementType": { - "$ref": "22" + "$ref": "30" }, "IsNullable": false }, @@ -674,27 +765,38 @@ "IsReadOnly": false }, { - "$id": "87", + "$id": "109", "Name": "floatExtensibleEnum", "SerializedName": "floatExtensibleEnum", "Description": "this is a float based extensible enum", "Type": { - "$ref": "26" + "$ref": "35" }, "IsRequired": false, "IsReadOnly": false }, { - "$id": "88", + "$id": "110", + "Name": "floatExtensibleEnumWithIntValue", + "SerializedName": "floatExtensibleEnumWithIntValue", + "Description": "this is a float based extensible enum", + "Type": { + "$ref": "40" + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "111", "Name": "floatExtensibleEnumCollection", "SerializedName": "floatExtensibleEnumCollection", "Description": "this is a collection of float based extensible enum", "Type": { - "$id": "89", + "$id": "112", "Kind": "Array", "Name": "Array", "ElementType": { - "$ref": "26" + "$ref": "35" }, "IsNullable": false }, @@ -702,27 +804,38 @@ "IsReadOnly": false }, { - "$id": "90", + "$id": "113", "Name": "floatFixedEnum", "SerializedName": "floatFixedEnum", "Description": "this is a float based fixed enum", "Type": { - "$ref": "30" + "$ref": "45" }, "IsRequired": false, "IsReadOnly": false }, { - "$id": "91", + "$id": "114", + "Name": "floatFixedEnumWithIntValue", + "SerializedName": "floatFixedEnumWithIntValue", + "Description": "this is a float based fixed enum", + "Type": { + "$ref": "50" + }, + "IsRequired": false, + "IsReadOnly": false + }, + { + "$id": "115", "Name": "floatFixedEnumCollection", "SerializedName": "floatFixedEnumCollection", "Description": "this is a collection of float based fixed enum", "Type": { - "$id": "92", + "$id": "116", "Kind": "Array", "Name": "Array", "ElementType": { - "$ref": "30" + "$ref": "45" }, "IsNullable": false }, @@ -730,27 +843,27 @@ "IsReadOnly": false }, { - "$id": "93", + "$id": "117", "Name": "intFixedEnum", "SerializedName": "intFixedEnum", "Description": "this is a int based fixed enum", "Type": { - "$ref": "34" + "$ref": "55" }, "IsRequired": false, "IsReadOnly": false }, { - "$id": "94", + "$id": "118", "Name": "intFixedEnumCollection", "SerializedName": "intFixedEnumCollection", "Description": "this is a collection of int based fixed enum", "Type": { - "$id": "95", + "$id": "119", "Kind": "Array", "Name": "Array", "ElementType": { - "$ref": "34" + "$ref": "55" }, "IsNullable": false }, @@ -758,63 +871,59 @@ "IsReadOnly": false }, { - "$id": "96", + "$id": "120", "Name": "stringFixedEnum", "SerializedName": "stringFixedEnum", "Description": "this is a string based fixed enum", "Type": { - "$ref": "14" + "$ref": "20" }, "IsRequired": false, "IsReadOnly": false }, { - "$id": "97", + "$id": "121", "Name": "requiredUnknown", "SerializedName": "requiredUnknown", "Description": "required unknown", "Type": { - "$id": "98", - "Kind": "Intrinsic", - "Name": "unknown", + "$id": "122", + "Kind": "any", "IsNullable": false }, "IsRequired": true, "IsReadOnly": false }, { - "$id": "99", + "$id": "123", "Name": "optionalUnknown", "SerializedName": "optionalUnknown", "Description": "optional unknown", "Type": { - "$id": "100", - "Kind": "Intrinsic", - "Name": "unknown", + "$id": "124", + "Kind": "any", "IsNullable": false }, "IsRequired": false, "IsReadOnly": false }, { - "$id": "101", + "$id": "125", "Name": "requiredRecordUnknown", "SerializedName": "requiredRecordUnknown", "Description": "required record of unknown", "Type": { - "$id": "102", + "$id": "126", "Kind": "Dictionary", "Name": "Dictionary", "KeyType": { - "$id": "103", - "Kind": "Primitive", - "Name": "String", + "$id": "127", + "Kind": "string", "IsNullable": false }, "ValueType": { - "$id": "104", - "Kind": "Intrinsic", - "Name": "unknown", + "$id": "128", + "Kind": "any", "IsNullable": false }, "IsNullable": false @@ -823,24 +932,22 @@ "IsReadOnly": false }, { - "$id": "105", + "$id": "129", "Name": "optionalRecordUnknown", "SerializedName": "optionalRecordUnknown", "Description": "optional record of unknown", "Type": { - "$id": "106", + "$id": "130", "Kind": "Dictionary", "Name": "Dictionary", "KeyType": { - "$id": "107", - "Kind": "Primitive", - "Name": "String", + "$id": "131", + "Kind": "string", "IsNullable": false }, "ValueType": { - "$id": "108", - "Kind": "Intrinsic", - "Name": "unknown", + "$id": "132", + "Kind": "any", "IsNullable": false }, "IsNullable": false @@ -849,24 +956,22 @@ "IsReadOnly": false }, { - "$id": "109", + "$id": "133", "Name": "readOnlyRequiredRecordUnknown", "SerializedName": "readOnlyRequiredRecordUnknown", "Description": "required readonly record of unknown", "Type": { - "$id": "110", + "$id": "134", "Kind": "Dictionary", "Name": "Dictionary", "KeyType": { - "$id": "111", - "Kind": "Primitive", - "Name": "String", + "$id": "135", + "Kind": "string", "IsNullable": false }, "ValueType": { - "$id": "112", - "Kind": "Intrinsic", - "Name": "unknown", + "$id": "136", + "Kind": "any", "IsNullable": false }, "IsNullable": false @@ -875,24 +980,22 @@ "IsReadOnly": true }, { - "$id": "113", + "$id": "137", "Name": "readOnlyOptionalRecordUnknown", "SerializedName": "readOnlyOptionalRecordUnknown", "Description": "optional readonly record of unknown", "Type": { - "$id": "114", + "$id": "138", "Kind": "Dictionary", "Name": "Dictionary", "KeyType": { - "$id": "115", - "Kind": "Primitive", - "Name": "String", + "$id": "139", + "Kind": "string", "IsNullable": false }, "ValueType": { - "$id": "116", - "Kind": "Intrinsic", - "Name": "unknown", + "$id": "140", + "Kind": "any", "IsNullable": false }, "IsNullable": false @@ -901,12 +1004,12 @@ "IsReadOnly": true }, { - "$id": "117", + "$id": "141", "Name": "modelWithRequiredNullable", "SerializedName": "modelWithRequiredNullable", "Description": "this is a model with required nullable properties", "Type": { - "$id": "118", + "$id": "142", "Kind": "Model", "Name": "ModelWithRequiredNullableProperties", "Namespace": "UnbrandedTypeSpec", @@ -915,37 +1018,36 @@ "Usage": "RoundTrip", "Properties": [ { - "$id": "119", + "$id": "143", "Name": "requiredNullablePrimitive", "SerializedName": "requiredNullablePrimitive", "Description": "required nullable primitive type", "Type": { - "$id": "120", - "Kind": "Primitive", - "Name": "Int32", + "$id": "144", + "Kind": "int32", "IsNullable": true }, "IsRequired": true, "IsReadOnly": false }, { - "$id": "121", + "$id": "145", "Name": "requiredExtensibleEnum", "SerializedName": "requiredExtensibleEnum", "Description": "required nullable extensible enum type", "Type": { - "$ref": "18" + "$ref": "25" }, "IsRequired": true, "IsReadOnly": false }, { - "$id": "122", + "$id": "146", "Name": "requiredFixedEnum", "SerializedName": "requiredFixedEnum", "Description": "required nullable fixed enum type", "Type": { - "$ref": "14" + "$ref": "20" }, "IsRequired": true, "IsReadOnly": false @@ -956,15 +1058,15 @@ "IsReadOnly": false }, { - "$id": "123", + "$id": "147", "Name": "requiredBytes", "SerializedName": "requiredBytes", "Description": "Required bytes", "Type": { - "$id": "124", - "Kind": "Primitive", - "Name": "Bytes", - "IsNullable": false + "$id": "148", + "Kind": "bytes", + "IsNullable": false, + "Encode": "base64" }, "IsRequired": true, "IsReadOnly": false @@ -972,10 +1074,10 @@ ] }, { - "$ref": "118" + "$ref": "142" }, { - "$id": "125", + "$id": "149", "Kind": "Model", "Name": "Friend", "Namespace": "UnbrandedTypeSpec", @@ -984,52 +1086,13 @@ "Usage": "RoundTrip", "Properties": [ { - "$id": "126", + "$id": "150", "Name": "name", "SerializedName": "name", "Description": "name of the NotFriend", "Type": { - "$id": "127", - "Kind": "Primitive", - "Name": "String", - "IsNullable": false - }, - "IsRequired": true, - "IsReadOnly": false - } - ] - }, - { - "$id": "128", - "Kind": "Model", - "Name": "ModelWithFormat", - "Namespace": "UnbrandedTypeSpec", - "IsNullable": false, - "Usage": "Input", - "Properties": [ - { - "$id": "129", - "Name": "sourceUrl", - "SerializedName": "sourceUrl", - "Description": "url format", - "Type": { - "$id": "130", - "Kind": "Primitive", - "Name": "Uri", - "IsNullable": false - }, - "IsRequired": true, - "IsReadOnly": false - }, - { - "$id": "131", - "Name": "guid", - "SerializedName": "guid", - "Description": "uuid format", - "Type": { - "$id": "132", - "Kind": "Primitive", - "Name": "Guid", + "$id": "151", + "Kind": "string", "IsNullable": false }, "IsRequired": true, @@ -1038,7 +1101,7 @@ ] }, { - "$id": "133", + "$id": "152", "Kind": "Model", "Name": "ProjectedModel", "Namespace": "UnbrandedTypeSpec", @@ -1047,14 +1110,13 @@ "Usage": "RoundTrip", "Properties": [ { - "$id": "134", + "$id": "153", "Name": "name", "SerializedName": "name", "Description": "name of the ModelWithProjectedName", "Type": { - "$id": "135", - "Kind": "Primitive", - "Name": "String", + "$id": "154", + "Kind": "string", "IsNullable": false }, "IsRequired": true, @@ -1063,7 +1125,7 @@ ] }, { - "$id": "136", + "$id": "155", "Kind": "Model", "Name": "ReturnsAnonymousModelResponse", "Namespace": "UnbrandedTypeSpec", @@ -1074,24 +1136,23 @@ ], "Clients": [ { - "$id": "137", + "$id": "156", "Name": "UnbrandedTypeSpecClient", "Description": "This is a sample typespec project.", "Operations": [ { - "$id": "138", + "$id": "157", "Name": "sayHi", "ResourceName": "UnbrandedTypeSpec", "Description": "Return hi", "Parameters": [ { - "$id": "139", + "$id": "158", "Name": "unbrandedTypeSpecUrl", "NameInRequest": "unbrandedTypeSpecUrl", "Type": { - "$id": "140", - "Kind": "Primitive", - "Name": "Uri", + "$id": "159", + "Kind": "uri", "IsNullable": false }, "Location": "Uri", @@ -1105,13 +1166,12 @@ "Kind": "Client" }, { - "$id": "141", + "$id": "160", "Name": "headParameter", "NameInRequest": "head-parameter", "Type": { - "$id": "142", - "Kind": "Primitive", - "Name": "String", + "$id": "161", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -1125,13 +1185,12 @@ "Kind": "Method" }, { - "$id": "143", + "$id": "162", "Name": "queryParameter", "NameInRequest": "queryParameter", "Type": { - "$id": "144", - "Kind": "Primitive", - "Name": "String", + "$id": "163", + "Kind": "string", "IsNullable": false }, "Location": "Query", @@ -1145,13 +1204,12 @@ "Kind": "Method" }, { - "$id": "145", + "$id": "164", "Name": "optionalQuery", "NameInRequest": "optionalQuery", "Type": { - "$id": "146", - "Kind": "Primitive", - "Name": "String", + "$id": "165", + "Kind": "string", "IsNullable": false }, "Location": "Query", @@ -1165,13 +1223,12 @@ "Kind": "Method" }, { - "$id": "147", + "$id": "166", "Name": "accept", "NameInRequest": "Accept", "Type": { - "$id": "148", - "Kind": "Primitive", - "Name": "String", + "$id": "167", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -1184,9 +1241,9 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "149", + "$id": "168", "Type": { - "$ref": "148" + "$ref": "167" }, "Value": "application/json" } @@ -1194,12 +1251,12 @@ ], "Responses": [ { - "$id": "150", + "$id": "169", "StatusCodes": [ 200 ], "BodyType": { - "$ref": "38" + "$ref": "60" }, "BodyMediaType": "Json", "Headers": [], @@ -1218,22 +1275,21 @@ "GenerateConvenienceMethod": true }, { - "$id": "151", + "$id": "170", "Name": "helloAgain", "ResourceName": "UnbrandedTypeSpec", "Description": "Return hi again", "Parameters": [ { - "$ref": "139" + "$ref": "158" }, { - "$id": "152", + "$id": "171", "Name": "p1", "NameInRequest": "p1", "Type": { - "$id": "153", - "Kind": "Primitive", - "Name": "String", + "$id": "172", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -1247,17 +1303,15 @@ "Kind": "Method" }, { - "$id": "154", + "$id": "173", "Name": "contentType", "NameInRequest": "content-type", "Type": { - "$id": "155", - "Kind": "Literal", - "Name": "Literal", - "LiteralValueType": { - "$id": "156", - "Kind": "Primitive", - "Name": "String", + "$id": "174", + "Kind": "constant", + "ValueType": { + "$id": "175", + "Kind": "string", "IsNullable": false }, "Value": "text/plain", @@ -1265,9 +1319,9 @@ }, "Location": "Header", "DefaultValue": { - "$id": "157", + "$id": "176", "Type": { - "$ref": "155" + "$ref": "174" }, "Value": "text/plain" }, @@ -1281,13 +1335,12 @@ "Kind": "Constant" }, { - "$id": "158", + "$id": "177", "Name": "p2", "NameInRequest": "p2", "Type": { - "$id": "159", - "Kind": "Primitive", - "Name": "String", + "$id": "178", + "Kind": "string", "IsNullable": false }, "Location": "Path", @@ -1301,11 +1354,11 @@ "Kind": "Method" }, { - "$id": "160", + "$id": "179", "Name": "action", "NameInRequest": "action", "Type": { - "$ref": "73" + "$ref": "95" }, "Location": "Body", "IsRequired": true, @@ -1318,13 +1371,12 @@ "Kind": "Method" }, { - "$id": "161", + "$id": "180", "Name": "accept", "NameInRequest": "Accept", "Type": { - "$id": "162", - "Kind": "Primitive", - "Name": "String", + "$id": "181", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -1337,9 +1389,9 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "163", + "$id": "182", "Type": { - "$ref": "162" + "$ref": "181" }, "Value": "application/json" } @@ -1347,12 +1399,12 @@ ], "Responses": [ { - "$id": "164", + "$id": "183", "StatusCodes": [ 200 ], "BodyType": { - "$ref": "73" + "$ref": "95" }, "BodyMediaType": "Json", "Headers": [], @@ -1374,22 +1426,21 @@ "GenerateConvenienceMethod": true }, { - "$id": "165", + "$id": "184", "Name": "noContentType", "ResourceName": "UnbrandedTypeSpec", "Description": "Return hi again", "Parameters": [ { - "$ref": "139" + "$ref": "158" }, { - "$id": "166", + "$id": "185", "Name": "p1", "NameInRequest": "p1", "Type": { - "$id": "167", - "Kind": "Primitive", - "Name": "String", + "$id": "186", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -1403,13 +1454,12 @@ "Kind": "Method" }, { - "$id": "168", + "$id": "187", "Name": "p2", "NameInRequest": "p2", "Type": { - "$id": "169", - "Kind": "Primitive", - "Name": "String", + "$id": "188", + "Kind": "string", "IsNullable": false }, "Location": "Path", @@ -1423,11 +1473,11 @@ "Kind": "Method" }, { - "$id": "170", + "$id": "189", "Name": "action", "NameInRequest": "action", "Type": { - "$ref": "73" + "$ref": "95" }, "Location": "Body", "IsRequired": true, @@ -1440,13 +1490,12 @@ "Kind": "Method" }, { - "$id": "171", + "$id": "190", "Name": "accept", "NameInRequest": "Accept", "Type": { - "$id": "172", - "Kind": "Primitive", - "Name": "String", + "$id": "191", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -1459,21 +1508,20 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "173", + "$id": "192", "Type": { - "$ref": "172" + "$ref": "191" }, "Value": "application/json" } }, { - "$id": "174", + "$id": "193", "Name": "contentType", "NameInRequest": "Content-Type", "Type": { - "$id": "175", - "Kind": "Primitive", - "Name": "String", + "$id": "194", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -1486,9 +1534,9 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "176", + "$id": "195", "Type": { - "$ref": "175" + "$ref": "194" }, "Value": "application/json" } @@ -1496,12 +1544,12 @@ ], "Responses": [ { - "$id": "177", + "$id": "196", "StatusCodes": [ 200 ], "BodyType": { - "$ref": "73" + "$ref": "95" }, "BodyMediaType": "Json", "Headers": [], @@ -1523,22 +1571,21 @@ "GenerateConvenienceMethod": false }, { - "$id": "178", + "$id": "197", "Name": "helloDemo2", "ResourceName": "UnbrandedTypeSpec", "Description": "Return hi in demo2", "Parameters": [ { - "$ref": "139" + "$ref": "158" }, { - "$id": "179", + "$id": "198", "Name": "accept", "NameInRequest": "Accept", "Type": { - "$id": "180", - "Kind": "Primitive", - "Name": "String", + "$id": "199", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -1551,9 +1598,9 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "181", + "$id": "200", "Type": { - "$ref": "180" + "$ref": "199" }, "Value": "application/json" } @@ -1561,12 +1608,12 @@ ], "Responses": [ { - "$id": "182", + "$id": "201", "StatusCodes": [ 200 ], "BodyType": { - "$ref": "38" + "$ref": "60" }, "BodyMediaType": "Json", "Headers": [], @@ -1585,20 +1632,20 @@ "GenerateConvenienceMethod": true }, { - "$id": "183", + "$id": "202", "Name": "createLiteral", "ResourceName": "UnbrandedTypeSpec", "Description": "Create with literal value", "Parameters": [ { - "$ref": "139" + "$ref": "158" }, { - "$id": "184", + "$id": "203", "Name": "body", "NameInRequest": "body", "Type": { - "$ref": "38" + "$ref": "60" }, "Location": "Body", "IsRequired": true, @@ -1611,13 +1658,12 @@ "Kind": "Method" }, { - "$id": "185", + "$id": "204", "Name": "accept", "NameInRequest": "Accept", "Type": { - "$id": "186", - "Kind": "Primitive", - "Name": "String", + "$id": "205", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -1630,21 +1676,20 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "187", + "$id": "206", "Type": { - "$ref": "186" + "$ref": "205" }, "Value": "application/json" } }, { - "$id": "188", + "$id": "207", "Name": "contentType", "NameInRequest": "Content-Type", "Type": { - "$id": "189", - "Kind": "Primitive", - "Name": "String", + "$id": "208", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -1657,9 +1702,9 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "190", + "$id": "209", "Type": { - "$ref": "189" + "$ref": "208" }, "Value": "application/json" } @@ -1667,12 +1712,12 @@ ], "Responses": [ { - "$id": "191", + "$id": "210", "StatusCodes": [ 200 ], "BodyType": { - "$ref": "38" + "$ref": "60" }, "BodyMediaType": "Json", "Headers": [], @@ -1694,26 +1739,24 @@ "GenerateConvenienceMethod": true }, { - "$id": "192", + "$id": "211", "Name": "helloLiteral", "ResourceName": "UnbrandedTypeSpec", "Description": "Send literal parameters", "Parameters": [ { - "$ref": "139" + "$ref": "158" }, { - "$id": "193", + "$id": "212", "Name": "p1", "NameInRequest": "p1", "Type": { - "$id": "194", - "Kind": "Literal", - "Name": "Literal", - "LiteralValueType": { - "$id": "195", - "Kind": "Primitive", - "Name": "String", + "$id": "213", + "Kind": "constant", + "ValueType": { + "$id": "214", + "Kind": "string", "IsNullable": false }, "Value": "test", @@ -1721,9 +1764,9 @@ }, "Location": "Header", "DefaultValue": { - "$id": "196", + "$id": "215", "Type": { - "$ref": "194" + "$ref": "213" }, "Value": "test" }, @@ -1737,17 +1780,15 @@ "Kind": "Constant" }, { - "$id": "197", + "$id": "216", "Name": "p2", "NameInRequest": "p2", "Type": { - "$id": "198", - "Kind": "Literal", - "Name": "Literal", - "LiteralValueType": { - "$id": "199", - "Kind": "Primitive", - "Name": "Int32", + "$id": "217", + "Kind": "constant", + "ValueType": { + "$id": "218", + "Kind": "int32", "IsNullable": false }, "Value": 123, @@ -1755,9 +1796,9 @@ }, "Location": "Path", "DefaultValue": { - "$id": "200", + "$id": "219", "Type": { - "$ref": "198" + "$ref": "217" }, "Value": 123 }, @@ -1771,17 +1812,15 @@ "Kind": "Constant" }, { - "$id": "201", + "$id": "220", "Name": "p3", "NameInRequest": "p3", "Type": { - "$id": "202", - "Kind": "Literal", - "Name": "Literal", - "LiteralValueType": { - "$id": "203", - "Kind": "Primitive", - "Name": "Boolean", + "$id": "221", + "Kind": "constant", + "ValueType": { + "$id": "222", + "Kind": "boolean", "IsNullable": false }, "Value": true, @@ -1789,9 +1828,9 @@ }, "Location": "Query", "DefaultValue": { - "$id": "204", + "$id": "223", "Type": { - "$ref": "202" + "$ref": "221" }, "Value": true }, @@ -1805,13 +1844,12 @@ "Kind": "Constant" }, { - "$id": "205", + "$id": "224", "Name": "accept", "NameInRequest": "Accept", "Type": { - "$id": "206", - "Kind": "Primitive", - "Name": "String", + "$id": "225", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -1824,9 +1862,9 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "207", + "$id": "226", "Type": { - "$ref": "206" + "$ref": "225" }, "Value": "application/json" } @@ -1834,12 +1872,12 @@ ], "Responses": [ { - "$id": "208", + "$id": "227", "StatusCodes": [ 200 ], "BodyType": { - "$ref": "38" + "$ref": "60" }, "BodyMediaType": "Json", "Headers": [], @@ -1858,23 +1896,28 @@ "GenerateConvenienceMethod": true }, { - "$id": "209", + "$id": "228", "Name": "topAction", "ResourceName": "UnbrandedTypeSpec", "Description": "top level method", "Parameters": [ { - "$ref": "139" + "$ref": "158" }, { - "$id": "210", + "$id": "229", "Name": "action", "NameInRequest": "action", "Type": { - "$id": "211", - "Kind": "Primitive", - "Name": "DateTime", - "IsNullable": false + "$id": "230", + "Kind": "utcDateTime", + "IsNullable": false, + "Encode": "rfc3339", + "WireType": { + "$id": "231", + "Kind": "string", + "IsNullable": false + } }, "Location": "Path", "IsRequired": true, @@ -1887,13 +1930,12 @@ "Kind": "Method" }, { - "$id": "212", + "$id": "232", "Name": "accept", "NameInRequest": "Accept", "Type": { - "$id": "213", - "Kind": "Primitive", - "Name": "String", + "$id": "233", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -1906,9 +1948,9 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "214", + "$id": "234", "Type": { - "$ref": "213" + "$ref": "233" }, "Value": "application/json" } @@ -1916,12 +1958,12 @@ ], "Responses": [ { - "$id": "215", + "$id": "235", "StatusCodes": [ 200 ], "BodyType": { - "$ref": "38" + "$ref": "60" }, "BodyMediaType": "Json", "Headers": [], @@ -1940,22 +1982,21 @@ "GenerateConvenienceMethod": true }, { - "$id": "216", + "$id": "236", "Name": "topAction2", "ResourceName": "UnbrandedTypeSpec", "Description": "top level method2", "Parameters": [ { - "$ref": "139" + "$ref": "158" }, { - "$id": "217", + "$id": "237", "Name": "accept", "NameInRequest": "Accept", "Type": { - "$id": "218", - "Kind": "Primitive", - "Name": "String", + "$id": "238", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -1968,9 +2009,9 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "219", + "$id": "239", "Type": { - "$ref": "218" + "$ref": "238" }, "Value": "application/json" } @@ -1978,12 +2019,12 @@ ], "Responses": [ { - "$id": "220", + "$id": "240", "StatusCodes": [ 200 ], "BodyType": { - "$ref": "38" + "$ref": "60" }, "BodyMediaType": "Json", "Headers": [], @@ -2002,20 +2043,20 @@ "GenerateConvenienceMethod": false }, { - "$id": "221", + "$id": "241", "Name": "patchAction", "ResourceName": "UnbrandedTypeSpec", "Description": "top level patch", "Parameters": [ { - "$ref": "139" + "$ref": "158" }, { - "$id": "222", + "$id": "242", "Name": "body", "NameInRequest": "body", "Type": { - "$ref": "38" + "$ref": "60" }, "Location": "Body", "IsRequired": true, @@ -2028,13 +2069,12 @@ "Kind": "Method" }, { - "$id": "223", + "$id": "243", "Name": "accept", "NameInRequest": "Accept", "Type": { - "$id": "224", - "Kind": "Primitive", - "Name": "String", + "$id": "244", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -2047,21 +2087,20 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "225", + "$id": "245", "Type": { - "$ref": "224" + "$ref": "244" }, "Value": "application/json" } }, { - "$id": "226", + "$id": "246", "Name": "contentType", "NameInRequest": "Content-Type", "Type": { - "$id": "227", - "Kind": "Primitive", - "Name": "String", + "$id": "247", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -2074,9 +2113,9 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "228", + "$id": "248", "Type": { - "$ref": "227" + "$ref": "247" }, "Value": "application/json" } @@ -2084,12 +2123,12 @@ ], "Responses": [ { - "$id": "229", + "$id": "249", "StatusCodes": [ 200 ], "BodyType": { - "$ref": "38" + "$ref": "60" }, "BodyMediaType": "Json", "Headers": [], @@ -2111,21 +2150,21 @@ "GenerateConvenienceMethod": false }, { - "$id": "230", + "$id": "250", "Name": "anonymousBody", "ResourceName": "UnbrandedTypeSpec", "Description": "body parameter without body decorator", "Parameters": [ { - "$ref": "139" + "$ref": "158" }, { - "$id": "231", + "$id": "251", "Name": "Thing", "NameInRequest": "Thing", "Description": "A model with a few properties of literal types", "Type": { - "$ref": "38" + "$ref": "60" }, "Location": "Body", "IsRequired": true, @@ -2138,13 +2177,12 @@ "Kind": "Method" }, { - "$id": "232", + "$id": "252", "Name": "accept", "NameInRequest": "Accept", "Type": { - "$id": "233", - "Kind": "Primitive", - "Name": "String", + "$id": "253", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -2157,21 +2195,20 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "234", + "$id": "254", "Type": { - "$ref": "233" + "$ref": "253" }, "Value": "application/json" } }, { - "$id": "235", + "$id": "255", "Name": "contentType", "NameInRequest": "Content-Type", "Type": { - "$id": "236", - "Kind": "Primitive", - "Name": "String", + "$id": "256", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -2184,9 +2221,9 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "237", + "$id": "257", "Type": { - "$ref": "236" + "$ref": "256" }, "Value": "application/json" } @@ -2194,12 +2231,12 @@ ], "Responses": [ { - "$id": "238", + "$id": "258", "StatusCodes": [ 200 ], "BodyType": { - "$ref": "38" + "$ref": "60" }, "BodyMediaType": "Json", "Headers": [], @@ -2221,21 +2258,21 @@ "GenerateConvenienceMethod": true }, { - "$id": "239", + "$id": "259", "Name": "friendlyModel", "ResourceName": "UnbrandedTypeSpec", "Description": "Model can have its friendly name", "Parameters": [ { - "$ref": "139" + "$ref": "158" }, { - "$id": "240", + "$id": "260", "Name": "Friend", "NameInRequest": "NotFriend", "Description": "this is not a friendly model but with a friendly name", "Type": { - "$ref": "125" + "$ref": "149" }, "Location": "Body", "IsRequired": true, @@ -2248,13 +2285,12 @@ "Kind": "Method" }, { - "$id": "241", + "$id": "261", "Name": "accept", "NameInRequest": "Accept", "Type": { - "$id": "242", - "Kind": "Primitive", - "Name": "String", + "$id": "262", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -2267,21 +2303,20 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "243", + "$id": "263", "Type": { - "$ref": "242" + "$ref": "262" }, "Value": "application/json" } }, { - "$id": "244", + "$id": "264", "Name": "contentType", "NameInRequest": "Content-Type", "Type": { - "$id": "245", - "Kind": "Primitive", - "Name": "String", + "$id": "265", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -2294,9 +2329,9 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "246", + "$id": "266", "Type": { - "$ref": "245" + "$ref": "265" }, "Value": "application/json" } @@ -2304,12 +2339,12 @@ ], "Responses": [ { - "$id": "247", + "$id": "267", "StatusCodes": [ 200 ], "BodyType": { - "$ref": "125" + "$ref": "149" }, "BodyMediaType": "Json", "Headers": [], @@ -2331,22 +2366,27 @@ "GenerateConvenienceMethod": true }, { - "$id": "248", + "$id": "268", "Name": "addTimeHeader", "ResourceName": "UnbrandedTypeSpec", "Parameters": [ { - "$ref": "139" + "$ref": "158" }, { - "$id": "249", + "$id": "269", "Name": "repeatabilityFirstSent", "NameInRequest": "Repeatability-First-Sent", "Type": { - "$id": "250", - "Kind": "Primitive", - "Name": "DateTimeRFC7231", - "IsNullable": false + "$id": "270", + "Kind": "utcDateTime", + "IsNullable": false, + "Encode": "rfc7231", + "WireType": { + "$id": "271", + "Kind": "string", + "IsNullable": false + } }, "Location": "Header", "IsRequired": false, @@ -2359,13 +2399,12 @@ "Kind": "Method" }, { - "$id": "251", + "$id": "272", "Name": "accept", "NameInRequest": "Accept", "Type": { - "$id": "252", - "Kind": "Primitive", - "Name": "String", + "$id": "273", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -2378,9 +2417,9 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "253", + "$id": "274", "Type": { - "$ref": "252" + "$ref": "273" }, "Value": "application/json" } @@ -2388,7 +2427,7 @@ ], "Responses": [ { - "$id": "254", + "$id": "275", "StatusCodes": [ 204 ], @@ -2406,144 +2445,21 @@ "GenerateConvenienceMethod": true }, { - "$id": "255", - "Name": "stringFormat", - "ResourceName": "UnbrandedTypeSpec", - "Description": "parameter has string format.", - "Parameters": [ - { - "$ref": "139" - }, - { - "$id": "256", - "Name": "subscriptionId", - "NameInRequest": "subscriptionId", - "Type": { - "$id": "257", - "Kind": "Primitive", - "Name": "Guid", - "IsNullable": false - }, - "Location": "Path", - "IsRequired": true, - "IsApiVersion": false, - "IsResourceParameter": false, - "IsContentType": false, - "IsEndpoint": false, - "SkipUrlEncoding": false, - "Explode": false, - "Kind": "Method" - }, - { - "$id": "258", - "Name": "body", - "NameInRequest": "body", - "Type": { - "$ref": "128" - }, - "Location": "Body", - "IsRequired": true, - "IsApiVersion": false, - "IsResourceParameter": false, - "IsContentType": false, - "IsEndpoint": false, - "SkipUrlEncoding": false, - "Explode": false, - "Kind": "Method" - }, - { - "$id": "259", - "Name": "contentType", - "NameInRequest": "Content-Type", - "Type": { - "$id": "260", - "Kind": "Primitive", - "Name": "String", - "IsNullable": false - }, - "Location": "Header", - "IsApiVersion": false, - "IsResourceParameter": false, - "IsContentType": true, - "IsRequired": true, - "IsEndpoint": false, - "SkipUrlEncoding": false, - "Explode": false, - "Kind": "Constant", - "DefaultValue": { - "$id": "261", - "Type": { - "$ref": "260" - }, - "Value": "application/json" - } - }, - { - "$id": "262", - "Name": "accept", - "NameInRequest": "Accept", - "Type": { - "$id": "263", - "Kind": "Primitive", - "Name": "String", - "IsNullable": false - }, - "Location": "Header", - "IsApiVersion": false, - "IsResourceParameter": false, - "IsContentType": false, - "IsRequired": true, - "IsEndpoint": false, - "SkipUrlEncoding": false, - "Explode": false, - "Kind": "Constant", - "DefaultValue": { - "$id": "264", - "Type": { - "$ref": "263" - }, - "Value": "application/json" - } - } - ], - "Responses": [ - { - "$id": "265", - "StatusCodes": [ - 204 - ], - "BodyMediaType": "Json", - "Headers": [], - "IsErrorResponse": false - } - ], - "HttpMethod": "POST", - "RequestBodyMediaType": "Json", - "Uri": "{unbrandedTypeSpecUrl}", - "Path": "/stringFormat/{subscriptionId}", - "RequestMediaTypes": [ - "application/json" - ], - "BufferResponse": true, - "GenerateProtocolMethod": true, - "GenerateConvenienceMethod": true - }, - { - "$id": "266", + "$id": "276", "Name": "projectedNameModel", "ResourceName": "UnbrandedTypeSpec", "Description": "Model can have its projected name", "Parameters": [ { - "$ref": "139" + "$ref": "158" }, { - "$id": "267", + "$id": "277", "Name": "ProjectedModel", "NameInRequest": "ModelWithProjectedName", "Description": "this is a model with a projected name", "Type": { - "$ref": "133" + "$ref": "152" }, "Location": "Body", "IsRequired": true, @@ -2556,13 +2472,12 @@ "Kind": "Method" }, { - "$id": "268", + "$id": "278", "Name": "accept", "NameInRequest": "Accept", "Type": { - "$id": "269", - "Kind": "Primitive", - "Name": "String", + "$id": "279", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -2575,21 +2490,20 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "270", + "$id": "280", "Type": { - "$ref": "269" + "$ref": "279" }, "Value": "application/json" } }, { - "$id": "271", + "$id": "281", "Name": "contentType", "NameInRequest": "Content-Type", "Type": { - "$id": "272", - "Kind": "Primitive", - "Name": "String", + "$id": "282", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -2602,9 +2516,9 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "273", + "$id": "283", "Type": { - "$ref": "272" + "$ref": "282" }, "Value": "application/json" } @@ -2612,12 +2526,12 @@ ], "Responses": [ { - "$id": "274", + "$id": "284", "StatusCodes": [ 200 ], "BodyType": { - "$ref": "133" + "$ref": "152" }, "BodyMediaType": "Json", "Headers": [], @@ -2639,22 +2553,21 @@ "GenerateConvenienceMethod": true }, { - "$id": "275", + "$id": "285", "Name": "returnsAnonymousModel", "ResourceName": "UnbrandedTypeSpec", "Description": "return anonymous model", "Parameters": [ { - "$ref": "139" + "$ref": "158" }, { - "$id": "276", + "$id": "286", "Name": "accept", "NameInRequest": "Accept", "Type": { - "$id": "277", - "Kind": "Primitive", - "Name": "String", + "$id": "287", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -2667,9 +2580,9 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "278", + "$id": "288", "Type": { - "$ref": "277" + "$ref": "287" }, "Value": "application/json" } @@ -2677,12 +2590,12 @@ ], "Responses": [ { - "$id": "279", + "$id": "289", "StatusCodes": [ 200 ], "BodyType": { - "$ref": "136" + "$ref": "155" }, "BodyMediaType": "Json", "Headers": [], @@ -2701,22 +2614,21 @@ "GenerateConvenienceMethod": true }, { - "$id": "280", + "$id": "290", "Name": "getUnknownValue", "ResourceName": "UnbrandedTypeSpec", "Description": "get extensible enum", "Parameters": [ { - "$ref": "139" + "$ref": "158" }, { - "$id": "281", + "$id": "291", "Name": "accept", "NameInRequest": "Accept", "Type": { - "$id": "282", - "Kind": "Primitive", - "Name": "String", + "$id": "292", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -2729,9 +2641,9 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "283", + "$id": "293", "Type": { - "$ref": "282" + "$ref": "292" }, "Value": "application/json" } @@ -2739,14 +2651,13 @@ ], "Responses": [ { - "$id": "284", + "$id": "294", "StatusCodes": [ 200 ], "BodyType": { - "$id": "285", - "Kind": "Primitive", - "Name": "String", + "$id": "295", + "Kind": "string", "IsNullable": false }, "BodyMediaType": "Json", @@ -2766,20 +2677,20 @@ "GenerateConvenienceMethod": true }, { - "$id": "286", + "$id": "296", "Name": "internalProtocol", "ResourceName": "UnbrandedTypeSpec", "Description": "When set protocol false and convenient true, then the protocol method should be internal", "Parameters": [ { - "$ref": "139" + "$ref": "158" }, { - "$id": "287", + "$id": "297", "Name": "body", "NameInRequest": "body", "Type": { - "$ref": "38" + "$ref": "60" }, "Location": "Body", "IsRequired": true, @@ -2792,13 +2703,12 @@ "Kind": "Method" }, { - "$id": "288", + "$id": "298", "Name": "accept", "NameInRequest": "Accept", "Type": { - "$id": "289", - "Kind": "Primitive", - "Name": "String", + "$id": "299", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -2811,21 +2721,20 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "290", + "$id": "300", "Type": { - "$ref": "289" + "$ref": "299" }, "Value": "application/json" } }, { - "$id": "291", + "$id": "301", "Name": "contentType", "NameInRequest": "Content-Type", "Type": { - "$id": "292", - "Kind": "Primitive", - "Name": "String", + "$id": "302", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -2838,9 +2747,9 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "293", + "$id": "303", "Type": { - "$ref": "292" + "$ref": "302" }, "Value": "application/json" } @@ -2848,12 +2757,12 @@ ], "Responses": [ { - "$id": "294", + "$id": "304", "StatusCodes": [ 200 ], "BodyType": { - "$ref": "38" + "$ref": "60" }, "BodyMediaType": "Json", "Headers": [], @@ -2875,22 +2784,21 @@ "GenerateConvenienceMethod": true }, { - "$id": "295", + "$id": "305", "Name": "stillConvenient", "ResourceName": "UnbrandedTypeSpec", "Description": "When set protocol false and convenient true, the convenient method should be generated even it has the same signature as protocol one", "Parameters": [ { - "$ref": "139" + "$ref": "158" }, { - "$id": "296", + "$id": "306", "Name": "accept", "NameInRequest": "Accept", "Type": { - "$id": "297", - "Kind": "Primitive", - "Name": "String", + "$id": "307", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -2903,9 +2811,9 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "298", + "$id": "308", "Type": { - "$ref": "297" + "$ref": "307" }, "Value": "application/json" } @@ -2913,7 +2821,7 @@ ], "Responses": [ { - "$id": "299", + "$id": "309", "StatusCodes": [ 204 ], @@ -2931,22 +2839,21 @@ "GenerateConvenienceMethod": true }, { - "$id": "300", + "$id": "310", "Name": "headAsBoolean", "ResourceName": "UnbrandedTypeSpec", "Description": "head as boolean.", "Parameters": [ { - "$ref": "139" + "$ref": "158" }, { - "$id": "301", + "$id": "311", "Name": "id", "NameInRequest": "id", "Type": { - "$id": "302", - "Kind": "Primitive", - "Name": "String", + "$id": "312", + "Kind": "string", "IsNullable": false }, "Location": "Path", @@ -2960,13 +2867,12 @@ "Kind": "Method" }, { - "$id": "303", + "$id": "313", "Name": "accept", "NameInRequest": "Accept", "Type": { - "$id": "304", - "Kind": "Primitive", - "Name": "String", + "$id": "314", + "Kind": "string", "IsNullable": false }, "Location": "Header", @@ -2979,9 +2885,9 @@ "Explode": false, "Kind": "Constant", "DefaultValue": { - "$id": "305", + "$id": "315", "Type": { - "$ref": "304" + "$ref": "314" }, "Value": "application/json" } @@ -2989,7 +2895,7 @@ ], "Responses": [ { - "$id": "306", + "$id": "316", "StatusCodes": [ 204 ], @@ -3008,20 +2914,20 @@ } ], "Protocol": { - "$id": "307" + "$id": "317" }, "Creatable": true, "Parameters": [ { - "$ref": "139" + "$ref": "158" } ] } ], "Auth": { - "$id": "308", + "$id": "318", "ApiKey": { - "$id": "309", + "$id": "319", "Name": "my-api-key" } } diff --git a/packages/http/CHANGELOG.md b/packages/http/CHANGELOG.md index 960139e7f01..0aacae8bb85 100644 --- a/packages/http/CHANGELOG.md +++ b/packages/http/CHANGELOG.md @@ -1,5 +1,28 @@ # Change Log - @typespec/http +## 0.57.0 + +### Bug Fixes + +- [#3022](https://github.com/microsoft/typespec/pull/3022) Update Flow Template to make use of the new array values + +### Bump dependencies + +- [#3401](https://github.com/microsoft/typespec/pull/3401) Update dependencies - May 2024 + +### Features + +- [#3342](https://github.com/microsoft/typespec/pull/3342) Add new multipart handling. Using `@multipartBody` with `HttpPart`. See [multipart docs](https://typespec.io/docs/next/libraries/http/multipart) for more information. + + ```tsp + op upload(@header contentType: "multipart/mixed", @multipartBody body: { + name: HttpPart; + avatar: HttpPart[]; + }): void; + ``` +- [#3462](https://github.com/microsoft/typespec/pull/3462) Use new compiler automatic `all` ruleset instead of explicitly provided one + + ## 0.56.0 ### Bug Fixes diff --git a/packages/http/README.md b/packages/http/README.md index 06511ac3d5a..488f8d78559 100644 --- a/packages/http/README.md +++ b/packages/http/README.md @@ -44,6 +44,7 @@ Available ruleSets: - [`@head`](#@head) - [`@header`](#@header) - [`@includeInapplicableMetadataInPayload`](#@includeinapplicablemetadatainpayload) +- [`@multipartBody`](#@multipartbody) - [`@patch`](#@patch) - [`@path`](#@path) - [`@post`](#@post) @@ -272,6 +273,32 @@ Specify if inapplicable metadata should be included in the payload for the given | ----- | ----------------- | --------------------------------------------------------------- | | value | `valueof boolean` | If true, inapplicable metadata will be included in the payload. | +#### `@multipartBody` + +```typespec +@TypeSpec.Http.multipartBody +``` + +##### Target + +`ModelProperty` + +##### Parameters + +None + +##### Examples + +```tsp +op upload( + @header `content-type`: "multipart/form-data", + @multipartBody body: { + fullName: HttpPart; + headShots: HttpPart[]; + }, +): void; +``` + #### `@patch` Specify the HTTP verb for the target operation to be `PATCH`. @@ -509,8 +536,13 @@ None ##### Examples ```typespec -op read(): {@statusCode: 200, @body pet: Pet} -op create(): {@statusCode: 201 | 202} +op read(): { + @statusCode _: 200; + @body pet: Pet; +}; +op create(): { + @statusCode _: 201 | 202; +}; ``` #### `@useAuth` diff --git a/packages/http/generated-defs/TypeSpec.Http.Private.ts b/packages/http/generated-defs/TypeSpec.Http.Private.ts index c1bbdcf252e..c78126bd7a1 100644 --- a/packages/http/generated-defs/TypeSpec.Http.Private.ts +++ b/packages/http/generated-defs/TypeSpec.Http.Private.ts @@ -1,3 +1,12 @@ -import type { DecoratorContext, Model } from "@typespec/compiler"; +import type { DecoratorContext, Model, Type } from "@typespec/compiler"; export type PlainDataDecorator = (context: DecoratorContext, target: Model) => void; + +export type HttpFileDecorator = (context: DecoratorContext, target: Model) => void; + +export type HttpPartDecorator = ( + context: DecoratorContext, + target: Model, + type: Type, + options: unknown +) => void; diff --git a/packages/http/generated-defs/TypeSpec.Http.ts b/packages/http/generated-defs/TypeSpec.Http.ts index 6058763fc1e..2e90bed1374 100644 --- a/packages/http/generated-defs/TypeSpec.Http.ts +++ b/packages/http/generated-defs/TypeSpec.Http.ts @@ -12,8 +12,13 @@ import type { * * @example * ```typespec - * op read(): {@statusCode: 200, @body pet: Pet} - * op create(): {@statusCode: 201 | 202} + * op read(): { + * @statusCode _: 200; + * @body pet: Pet; + * }; + * op create(): { + * @statusCode _: 201 | 202; + * }; * ``` */ export type StatusCodeDecorator = (context: DecoratorContext, target: ModelProperty) => void; @@ -111,6 +116,23 @@ export type BodyRootDecorator = (context: DecoratorContext, target: ModelPropert */ export type BodyIgnoreDecorator = (context: DecoratorContext, target: ModelProperty) => void; +/** + * + * + * + * @example + * ```tsp + * op upload( + * @header `content-type`: "multipart/form-data", + * @multipartBody body: { + * fullName: HttpPart, + * headShots: HttpPart[] + * } + * ): void; + * ``` + */ +export type MultipartBodyDecorator = (context: DecoratorContext, target: ModelProperty) => void; + /** * Specify the HTTP verb for the target operation to be `GET`. * diff --git a/packages/http/generated-defs/TypeSpec.Http.ts-test.ts b/packages/http/generated-defs/TypeSpec.Http.ts-test.ts index efd098a96a2..fcdfdf90781 100644 --- a/packages/http/generated-defs/TypeSpec.Http.ts-test.ts +++ b/packages/http/generated-defs/TypeSpec.Http.ts-test.ts @@ -8,6 +8,7 @@ import { $head, $header, $includeInapplicableMetadataInPayload, + $multipartBody, $patch, $path, $post, @@ -28,6 +29,7 @@ import type { HeadDecorator, HeaderDecorator, IncludeInapplicableMetadataInPayloadDecorator, + MultipartBodyDecorator, PatchDecorator, PathDecorator, PostDecorator, @@ -48,6 +50,7 @@ type Decorators = { $path: PathDecorator; $bodyRoot: BodyRootDecorator; $bodyIgnore: BodyIgnoreDecorator; + $multipartBody: MultipartBodyDecorator; $get: GetDecorator; $put: PutDecorator; $post: PostDecorator; @@ -70,6 +73,7 @@ const _: Decorators = { $path, $bodyRoot, $bodyIgnore, + $multipartBody, $get, $put, $post, diff --git a/packages/http/lib/http-decorators.tsp b/packages/http/lib/decorators.tsp similarity index 95% rename from packages/http/lib/http-decorators.tsp rename to packages/http/lib/decorators.tsp index f8631f23cbc..0ed94557013 100644 --- a/packages/http/lib/http-decorators.tsp +++ b/packages/http/lib/decorators.tsp @@ -122,14 +122,34 @@ extern dec bodyRoot(target: ModelProperty); */ extern dec bodyIgnore(target: ModelProperty); +/** + * @example + * + * ```tsp + * op upload( + * @header `content-type`: "multipart/form-data", + * @multipartBody body: { + * fullName: HttpPart, + * headShots: HttpPart[] + * } + * ): void; + * ``` + */ +extern dec multipartBody(target: ModelProperty); + /** * Specify the status code for this response. Property type must be a status code integer or a union of status code integer. * * @example * * ```typespec - * op read(): {@statusCode: 200, @body pet: Pet} - * op create(): {@statusCode: 201 | 202} + * op read(): { + * @statusCode _: 200; + * @body pet: Pet; + * }; + * op create(): { + * @statusCode _: 201 | 202; + * }; * ``` */ extern dec statusCode(target: ModelProperty); @@ -291,10 +311,3 @@ extern dec route( * ``` */ extern dec sharedRoute(target: Operation); - -/** - * Private decorators. Those are meant for internal use inside Http types only. - */ -namespace Private { - extern dec plainData(target: TypeSpec.Reflection.Model); -} diff --git a/packages/http/lib/http.tsp b/packages/http/lib/http.tsp index 1b1d8d75578..411a7c1f3d9 100644 --- a/packages/http/lib/http.tsp +++ b/packages/http/lib/http.tsp @@ -1,5 +1,6 @@ import "../dist/src/index.js"; -import "./http-decorators.tsp"; +import "./decorators.tsp"; +import "./private.decorators.tsp"; import "./auth.tsp"; namespace TypeSpec.Http; @@ -104,3 +105,18 @@ model ConflictResponse is Response<409>; model PlainData { ...Data; } + +@Private.httpFile +model File { + contentType?: string; + filename?: string; + contents: bytes; +} + +model HttpPartOptions { + /** Name of the part when using the array form. */ + name?: string; +} + +@Private.httpPart(Type, Options) +model HttpPart {} diff --git a/packages/http/lib/private.decorators.tsp b/packages/http/lib/private.decorators.tsp new file mode 100644 index 00000000000..87b46498706 --- /dev/null +++ b/packages/http/lib/private.decorators.tsp @@ -0,0 +1,14 @@ +import "../dist/src/private.decorators.js"; + +/** + * Private decorators. Those are meant for internal use inside Http types only. + */ +namespace TypeSpec.Http.Private; + +extern dec plainData(target: TypeSpec.Reflection.Model); +extern dec httpFile(target: TypeSpec.Reflection.Model); +extern dec httpPart( + target: TypeSpec.Reflection.Model, + type: unknown, + options: valueof HttpPartOptions +); diff --git a/packages/http/package.json b/packages/http/package.json index 18a1138d12a..088fcefcbe1 100644 --- a/packages/http/package.json +++ b/packages/http/package.json @@ -1,6 +1,6 @@ { "name": "@typespec/http", - "version": "0.56.0", + "version": "0.57.0", "author": "Microsoft Corporation", "description": "TypeSpec HTTP protocol binding", "homepage": "https://github.com/microsoft/typespec", diff --git a/packages/http/src/body.ts b/packages/http/src/body.ts deleted file mode 100644 index 462396bc4e3..00000000000 --- a/packages/http/src/body.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { - Diagnostic, - DuplicateTracker, - ModelProperty, - Program, - Type, - createDiagnosticCollector, - filterModelProperties, - getDiscriminator, - isArrayModelType, - navigateType, -} from "@typespec/compiler"; -import { - isBody, - isBodyRoot, - isHeader, - isPathParam, - isQueryParam, - isStatusCode, -} from "./decorators.js"; -import { createDiagnostic } from "./lib.js"; -import { Visibility, isVisible } from "./metadata.js"; - -export interface ResolvedBody { - readonly type: Type; - /** `true` if the body was specified with `@body` */ - readonly isExplicit: boolean; - /** If the body original model contained property annotated with metadata properties. */ - readonly containsMetadataAnnotations: boolean; - /** If body is defined with `@body` or `@bodyRoot` this is the property */ - readonly property?: ModelProperty; -} - -export function resolveBody( - program: Program, - requestOrResponseType: Type, - metadata: Set, - rootPropertyMap: Map, - visibility: Visibility, - usedIn: "request" | "response" -): [ResolvedBody | undefined, readonly Diagnostic[]] { - const diagnostics = createDiagnosticCollector(); - // non-model or intrinsic/array model -> response body is response type - if (requestOrResponseType.kind !== "Model" || isArrayModelType(program, requestOrResponseType)) { - return diagnostics.wrap({ - type: requestOrResponseType, - isExplicit: false, - containsMetadataAnnotations: false, - }); - } - - const duplicateTracker = new DuplicateTracker(); - - // look for explicit body - let resolvedBody: ResolvedBody | undefined; - for (const property of metadata) { - const isBodyVal = isBody(program, property); - const isBodyRootVal = isBodyRoot(program, property); - if (isBodyVal || isBodyRootVal) { - duplicateTracker.track("body", property); - let containsMetadataAnnotations = false; - if (isBodyVal) { - const valid = diagnostics.pipe(validateBodyProperty(program, property, usedIn)); - containsMetadataAnnotations = !valid; - } - if (resolvedBody === undefined) { - resolvedBody = { - type: property.type, - isExplicit: isBodyVal, - containsMetadataAnnotations, - property, - }; - } - } - } - for (const [_, items] of duplicateTracker.entries()) { - for (const prop of items) { - diagnostics.add( - createDiagnostic({ - code: "duplicate-body", - target: prop, - }) - ); - } - } - if (resolvedBody === undefined) { - // Special case if the model as a parent model then we'll return an empty object as this is assumed to be a nominal type. - // Special Case if the model has an indexer then it means it can return props so cannot be void. - if (requestOrResponseType.baseModel || requestOrResponseType.indexer) { - return diagnostics.wrap({ - type: requestOrResponseType, - isExplicit: false, - containsMetadataAnnotations: false, - }); - } - // Special case for legacy purposes if the return type is an empty model with only @discriminator("xyz") - // Then we still want to return that object as it technically always has a body with that implicit property. - if ( - requestOrResponseType.derivedModels.length > 0 && - getDiscriminator(program, requestOrResponseType) - ) { - return diagnostics.wrap({ - type: requestOrResponseType, - isExplicit: false, - containsMetadataAnnotations: false, - }); - } - } - - const bodyRoot = resolvedBody?.property ? rootPropertyMap.get(resolvedBody.property) : undefined; - - const unannotatedProperties = filterModelProperties( - program, - requestOrResponseType, - (p) => !metadata.has(p) && p !== bodyRoot && isVisible(program, p, visibility) - ); - - if (unannotatedProperties.properties.size > 0) { - if (resolvedBody === undefined) { - return diagnostics.wrap({ - type: unannotatedProperties, - isExplicit: false, - containsMetadataAnnotations: false, - }); - } else { - diagnostics.add( - createDiagnostic({ - code: "duplicate-body", - messageId: "bodyAndUnannotated", - target: requestOrResponseType, - }) - ); - } - } - - return diagnostics.wrap(resolvedBody); -} - -/** Validate a property marked with `@body` */ -export function validateBodyProperty( - program: Program, - property: ModelProperty, - usedIn: "request" | "response" -): [boolean, readonly Diagnostic[]] { - const diagnostics = createDiagnosticCollector(); - navigateType( - property.type, - { - modelProperty: (prop) => { - const kind = isHeader(program, prop) - ? "header" - : usedIn === "request" && isQueryParam(program, prop) - ? "query" - : usedIn === "request" && isPathParam(program, prop) - ? "path" - : usedIn === "response" && isStatusCode(program, prop) - ? "statusCode" - : undefined; - - if (kind) { - diagnostics.add( - createDiagnostic({ - code: "metadata-ignored", - format: { kind }, - target: prop, - }) - ); - } - }, - }, - {} - ); - return diagnostics.wrap(diagnostics.diagnostics.length === 0); -} diff --git a/packages/http/src/content-types.ts b/packages/http/src/content-types.ts index eaf9dc99c65..f7a8751a7a0 100644 --- a/packages/http/src/content-types.ts +++ b/packages/http/src/content-types.ts @@ -3,6 +3,7 @@ import { getHeaderFieldName } from "./decorators.js"; import { createDiagnostic } from "./lib.js"; /** + * @deprecated Use `OperationProperty.kind === 'contentType'` instead. * Check if the given model property is the content type header. * @param program Program * @param property Model property. @@ -39,6 +40,8 @@ export function getContentTypes(property: ModelProperty): [string[], readonly Di } return diagnostics.wrap(contentTypes); + } else if (property.type.kind === "Scalar" && property.type.name === "string") { + return [["*/*"], []]; } return [[], [createDiagnostic({ code: "content-type-string", target: property })]]; diff --git a/packages/http/src/decorators.ts b/packages/http/src/decorators.ts index 7325cd55315..a8473ed52b4 100644 --- a/packages/http/src/decorators.ts +++ b/packages/http/src/decorators.ts @@ -17,12 +17,10 @@ import { ignoreDiagnostics, isArrayModelType, reportDeprecated, - setTypeSpecNamespace, typespecTypeToJson, validateDecoratorTarget, validateDecoratorUniqueOnNode, } from "@typespec/compiler"; -import { PlainDataDecorator } from "../generated-defs/TypeSpec.Http.Private.js"; import { BodyDecorator, BodyIgnoreDecorator, @@ -31,6 +29,7 @@ import { GetDecorator, HeadDecorator, HeaderDecorator, + MultipartBodyDecorator, PatchDecorator, PathDecorator, PostDecorator, @@ -221,6 +220,17 @@ export function isBodyIgnore(program: Program, entity: ModelProperty): boolean { return program.stateSet(HttpStateKeys.bodyIgnore).has(entity); } +export const $multipartBody: MultipartBodyDecorator = ( + context: DecoratorContext, + entity: ModelProperty +) => { + context.program.stateSet(HttpStateKeys.multipartBody).add(entity); +}; + +export function isMultipartBodyProperty(program: Program, entity: Type): boolean { + return program.stateSet(HttpStateKeys.multipartBody).has(entity); +} + export const $statusCode: StatusCodeDecorator = ( context: DecoratorContext, entity: ModelProperty @@ -450,36 +460,6 @@ export function getServers(program: Program, type: Namespace): HttpServer[] | un return program.stateMap(HttpStateKeys.servers).get(type); } -export const $plainData: PlainDataDecorator = (context: DecoratorContext, entity: Model) => { - const { program } = context; - - const decoratorsToRemove = ["$header", "$body", "$query", "$path", "$statusCode"]; - const [headers, bodies, queries, paths, statusCodes] = [ - program.stateMap(HttpStateKeys.header), - program.stateSet(HttpStateKeys.body), - program.stateMap(HttpStateKeys.query), - program.stateMap(HttpStateKeys.path), - program.stateMap(HttpStateKeys.statusCode), - ]; - - for (const property of entity.properties.values()) { - // Remove the decorators so that they do not run in the future, for example, - // if this model is later spread into another. - property.decorators = property.decorators.filter( - (d) => !decoratorsToRemove.includes(d.decorator.name) - ); - - // Remove the impact the decorators already had on this model. - headers.delete(property); - bodies.delete(property); - queries.delete(property); - paths.delete(property); - statusCodes.delete(property); - } -}; - -setTypeSpecNamespace("Private", $plainData); - export function $useAuth( context: DecoratorContext, entity: Namespace | Interface | Operation, diff --git a/packages/http/src/http-property.ts b/packages/http/src/http-property.ts new file mode 100644 index 00000000000..fffa6b4047e --- /dev/null +++ b/packages/http/src/http-property.ts @@ -0,0 +1,220 @@ +import { + DiagnosticResult, + Model, + Type, + compilerAssert, + createDiagnosticCollector, + walkPropertiesInherited, + type Diagnostic, + type ModelProperty, + type Program, +} from "@typespec/compiler"; +import { Queue } from "@typespec/compiler/utils"; +import { + getHeaderFieldOptions, + getPathParamOptions, + getQueryParamOptions, + isBody, + isBodyRoot, + isMultipartBodyProperty, + isStatusCode, +} from "./decorators.js"; +import { createDiagnostic } from "./lib.js"; +import { Visibility, isVisible } from "./metadata.js"; +import { HeaderFieldOptions, PathParameterOptions, QueryParameterOptions } from "./types.js"; + +export type HttpProperty = + | HeaderProperty + | ContentTypeProperty + | QueryProperty + | PathProperty + | StatusCodeProperty + | BodyProperty + | BodyRootProperty + | MultipartBodyProperty + | BodyPropertyProperty; + +export interface HttpPropertyBase { + readonly property: ModelProperty; +} + +export interface HeaderProperty extends HttpPropertyBase { + readonly kind: "header"; + readonly options: HeaderFieldOptions; +} + +export interface ContentTypeProperty extends HttpPropertyBase { + readonly kind: "contentType"; +} + +export interface QueryProperty extends HttpPropertyBase { + readonly kind: "query"; + readonly options: QueryParameterOptions; +} +export interface PathProperty extends HttpPropertyBase { + readonly kind: "path"; + readonly options: PathParameterOptions; +} +export interface StatusCodeProperty extends HttpPropertyBase { + readonly kind: "statusCode"; +} +export interface BodyProperty extends HttpPropertyBase { + readonly kind: "body"; +} +export interface BodyRootProperty extends HttpPropertyBase { + readonly kind: "bodyRoot"; +} +export interface MultipartBodyProperty extends HttpPropertyBase { + readonly kind: "multipartBody"; +} +/** Property to include inside the body */ +export interface BodyPropertyProperty extends HttpPropertyBase { + readonly kind: "bodyProperty"; +} + +export interface GetHttpPropertyOptions { + isImplicitPathParam?: (param: ModelProperty) => boolean; +} +/** + * Find the type of a property in a model + */ +export function getHttpProperty( + program: Program, + property: ModelProperty, + options: GetHttpPropertyOptions = {} +): [HttpProperty, readonly Diagnostic[]] { + const diagnostics: Diagnostic[] = []; + function createResult(opts: T): [T, readonly Diagnostic[]] { + return [{ ...opts, property } as any, diagnostics]; + } + + const annotations = { + header: getHeaderFieldOptions(program, property), + query: getQueryParamOptions(program, property), + path: getPathParamOptions(program, property), + body: isBody(program, property), + bodyRoot: isBodyRoot(program, property), + multipartBody: isMultipartBodyProperty(program, property), + statusCode: isStatusCode(program, property), + }; + const defined = Object.entries(annotations).filter((x) => !!x[1]); + if (defined.length === 0) { + if (options.isImplicitPathParam && options.isImplicitPathParam(property)) { + return createResult({ + kind: "path", + options: { + name: property.name, + type: "path", + }, + property, + }); + } + return [{ kind: "bodyProperty", property }, []]; + } else if (defined.length > 1) { + diagnostics.push( + createDiagnostic({ + code: "operation-param-duplicate-type", + format: { paramName: property.name, types: defined.map((x) => x[0]).join(", ") }, + target: property, + }) + ); + } + + if (annotations.header) { + if (annotations.header.name.toLowerCase() === "content-type") { + return createResult({ kind: "contentType", property }); + } else { + return createResult({ kind: "header", options: annotations.header, property }); + } + } else if (annotations.query) { + return createResult({ kind: "query", options: annotations.query, property }); + } else if (annotations.path) { + return createResult({ kind: "path", options: annotations.path, property }); + } else if (annotations.statusCode) { + return createResult({ kind: "statusCode", property }); + } else if (annotations.body) { + return createResult({ kind: "body", property }); + } else if (annotations.bodyRoot) { + return createResult({ kind: "bodyRoot", property }); + } else if (annotations.multipartBody) { + return createResult({ kind: "multipartBody", property }); + } + compilerAssert(false, `Unexpected http property type`, property); +} + +/** + * Walks the given input(request parameters or response) and return all the properties and where they should be included(header, query, path, body, as a body property, etc.) + * + * @param rootMapOut If provided, the map will be populated to link nested metadata properties to their root properties. + */ +export function resolvePayloadProperties( + program: Program, + type: Type, + visibility: Visibility, + options: GetHttpPropertyOptions = {} +): DiagnosticResult { + const diagnostics = createDiagnosticCollector(); + const httpProperties = new Map(); + + if (type.kind !== "Model" || type.properties.size === 0) { + return diagnostics.wrap([]); + } + + const visited = new Set(); + const queue = new Queue<[Model, ModelProperty | undefined]>([[type, undefined]]); + + while (!queue.isEmpty()) { + const [model, rootOpt] = queue.dequeue(); + visited.add(model); + + for (const property of walkPropertiesInherited(model)) { + const root = rootOpt ?? property; + + if (!isVisible(program, property, visibility)) { + continue; + } + + let httpProperty = diagnostics.pipe(getHttpProperty(program, property, options)); + if (shouldTreatAsBodyProperty(httpProperty, visibility)) { + httpProperty = { kind: "bodyProperty", property }; + } + httpProperties.set(property, httpProperty); + if ( + property !== root && + (httpProperty.kind === "body" || + httpProperty.kind === "bodyRoot" || + httpProperty.kind === "multipartBody") + ) { + const parent = httpProperties.get(root); + if (parent?.kind === "bodyProperty") { + httpProperties.delete(root); + } + } + if (httpProperty.kind === "body" || httpProperty.kind === "multipartBody") { + continue; // We ignore any properties under `@body` or `@multipartBody` + } + + if ( + property.type.kind === "Model" && + !type.indexer && + type.properties.size > 0 && + !visited.has(property.type) + ) { + queue.enqueue([property.type, root]); + } + } + } + + return diagnostics.wrap([...httpProperties.values()]); +} + +function shouldTreatAsBodyProperty(property: HttpProperty, visibility: Visibility): boolean { + if (visibility & Visibility.Read) { + return property.kind === "query" || property.kind === "path"; + } + + if (!(visibility & Visibility.Read)) { + return property.kind === "statusCode"; + } + return false; +} diff --git a/packages/http/src/index.ts b/packages/http/src/index.ts index c3d8bcd8818..ee792cda39a 100644 --- a/packages/http/src/index.ts +++ b/packages/http/src/index.ts @@ -7,6 +7,7 @@ export * from "./decorators.js"; export * from "./metadata.js"; export * from "./operations.js"; export * from "./parameters.js"; +export { getHttpFileModel, isHttpFile, isOrExtendsHttpFile } from "./private.decorators.js"; export * from "./responses.js"; export * from "./route.js"; export * from "./types.js"; diff --git a/packages/http/src/lib.ts b/packages/http/src/lib.ts index 8d8ed02b7a3..a628270b5cc 100644 --- a/packages/http/src/lib.ts +++ b/packages/http/src/lib.ts @@ -117,12 +117,42 @@ export const $lib = createTypeSpecLibrary({ default: `@visibility("write") is not supported. Use @visibility("update"), @visibility("create") or @visibility("create", "update") as appropriate.`, }, }, + "multipart-invalid-content-type": { + severity: "error", + messages: { + default: paramMessage`Content type '${"contentType"}' is not a multipart content type. Supported content types are: ${"supportedContentTypes"}.`, + }, + }, "multipart-model": { severity: "error", messages: { default: "Multipart request body must be a model.", }, }, + "multipart-part": { + severity: "error", + messages: { + default: "Expect item to be an HttpPart model.", + }, + }, + "multipart-nested": { + severity: "error", + messages: { + default: "Cannot use @multipartBody inside of an HttpPart", + }, + }, + "http-file-extra-property": { + severity: "error", + messages: { + default: paramMessage`File model cannot define extra properties. Found '${"propName"}'.`, + }, + }, + "formdata-no-part-name": { + severity: "error", + messages: { + default: "Part used in multipart/form-data must have a name.", + }, + }, "header-format-required": { severity: "error", messages: { @@ -144,6 +174,7 @@ export const $lib = createTypeSpecLibrary({ body: { description: "State for the @body decorator" }, bodyRoot: { description: "State for the @bodyRoot decorator" }, bodyIgnore: { description: "State for the @bodyIgnore decorator" }, + multipartBody: { description: "State for the @bodyIgnore decorator" }, statusCode: { description: "State for the @statusCode decorator" }, verbs: { description: "State for the verb decorators (@get, @post, @put, etc.)" }, servers: { description: "State for the @server decorator" }, @@ -157,6 +188,10 @@ export const $lib = createTypeSpecLibrary({ routes: {}, sharedRoutes: { description: "State for the @sharedRoute decorator" }, routeOptions: {}, + + // private + file: { description: "State for the @Private.file decorator" }, + httpPart: { description: "State for the @Private.httpPart decorator" }, }, } as const); diff --git a/packages/http/src/linter.ts b/packages/http/src/linter.ts index 19f5dae93e1..c63f26562f2 100644 --- a/packages/http/src/linter.ts +++ b/packages/http/src/linter.ts @@ -3,11 +3,4 @@ import { opReferenceContainerRouteRule } from "./rules/op-reference-container-ro export const $linter = defineLinter({ rules: [opReferenceContainerRouteRule], - ruleSets: { - all: { - enable: { - [`@typespec/http/${opReferenceContainerRouteRule.name}`]: true, - }, - }, - }, }); diff --git a/packages/http/src/metadata.ts b/packages/http/src/metadata.ts index 5e2c0ad4c04..8bf409c3a69 100644 --- a/packages/http/src/metadata.ts +++ b/packages/http/src/metadata.ts @@ -1,6 +1,5 @@ import { compilerAssert, - DiagnosticCollector, getEffectiveModelType, getParameterVisibility, isVisible as isVisibleCore, @@ -8,18 +7,17 @@ import { ModelProperty, Operation, Program, - Queue, - TwoLevelMap, Type, Union, - walkPropertiesInherited, } from "@typespec/compiler"; +import { TwoLevelMap } from "@typespec/compiler/utils"; import { includeInapplicableMetadataInPayload, isBody, isBodyIgnore, isBodyRoot, isHeader, + isMultipartBodyProperty, isPathParam, isQueryParam, isStatusCode, @@ -219,72 +217,6 @@ export function resolveRequestVisibility( return visibility; } -/** - * Walks the given type and collects all applicable metadata and `@body` - * properties recursively. - * - * @param rootMapOut If provided, the map will be populated to link - * nested metadata properties to their root properties. - */ -export function gatherMetadata( - program: Program, - diagnostics: DiagnosticCollector, // currently unused, but reserved for future diagnostics - type: Type, - visibility: Visibility, - isMetadataCallback = isMetadata, - rootMapOut?: Map -): Set { - const metadata = new Map(); - if (type.kind !== "Model" || type.properties.size === 0) { - return new Set(); - } - - const visited = new Set(); - const queue = new Queue<[Model, ModelProperty | undefined]>([[type, undefined]]); - - while (!queue.isEmpty()) { - const [model, rootOpt] = queue.dequeue(); - visited.add(model); - - for (const property of walkPropertiesInherited(model)) { - const root = rootOpt ?? property; - - if (!isVisible(program, property, visibility)) { - continue; - } - - // ISSUE: This should probably be an error, but that's a breaking - // change that currently breaks some samples and tests. - // - // The traversal here is level-order so that the preferred metadata in - // the case of duplicates, which is the most compatible with prior - // behavior where nested metadata was always dropped. - if (metadata.has(property.name)) { - continue; - } - - if (isApplicableMetadataOrBody(program, property, visibility, isMetadataCallback)) { - metadata.set(property.name, property); - rootMapOut?.set(property, root); - if (isBody(program, property)) { - continue; // We ignore any properties under `@body` - } - } - - if ( - property.type.kind === "Model" && - !type.indexer && - type.properties.size > 0 && - !visited.has(property.type) - ) { - queue.enqueue([property.type, root]); - } - } - } - - return new Set(metadata.values()); -} - /** * Determines if a property is metadata. A property is defined to be * metadata if it is marked `@header`, `@query`, `@path`, or `@statusCode`. @@ -348,7 +280,12 @@ function isApplicableMetadataCore( return false; // no metadata is applicable to collection items } - if (treatBodyAsMetadata && (isBody(program, property) || isBodyRoot(program, property))) { + if ( + treatBodyAsMetadata && + (isBody(program, property) || + isBodyRoot(program, property) || + isMultipartBodyProperty(program, property)) + ) { return true; } @@ -602,7 +539,7 @@ export function createMetadataInfo(program: Program, options?: MetadataInfoOptio /** * If the type is an anonymous model, tries to find a named model that has the same - * set of properties when non-payload properties are excluded. + * set of properties when non-payload properties are excluded.we */ function getEffectivePayloadType(type: Type, visibility: Visibility): Type { if (type.kind === "Model" && !type.name) { diff --git a/packages/http/src/parameters.ts b/packages/http/src/parameters.ts index 1b26aa9ace9..9486c101938 100644 --- a/packages/http/src/parameters.ts +++ b/packages/http/src/parameters.ts @@ -5,23 +5,14 @@ import { Operation, Program, } from "@typespec/compiler"; -import { resolveBody, ResolvedBody } from "./body.js"; -import { getContentTypes, isContentTypeHeader } from "./content-types.js"; -import { - getHeaderFieldOptions, - getOperationVerb, - getPathParamOptions, - getQueryParamOptions, - isBody, - isBodyRoot, -} from "./decorators.js"; +import { getOperationVerb } from "./decorators.js"; import { createDiagnostic } from "./lib.js"; -import { gatherMetadata, isMetadata, resolveRequestVisibility } from "./metadata.js"; +import { resolveRequestVisibility } from "./metadata.js"; +import { resolveHttpPayload } from "./payload.js"; import { HttpOperation, HttpOperationParameter, HttpOperationParameters, - HttpOperationRequestBody, HttpVerb, OperationParameterOptions, } from "./types.js"; @@ -60,82 +51,49 @@ function getOperationParametersForVerb( ): [HttpOperationParameters, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); const visibility = resolveRequestVisibility(program, operation, verb); - const rootPropertyMap = new Map(); - const metadata = gatherMetadata( - program, - diagnostics, - operation.parameters, - visibility, - (_, param) => isMetadata(program, param) || isImplicitPathParam(param), - rootPropertyMap - ); - function isImplicitPathParam(param: ModelProperty) { const isTopLevel = param.model === operation.parameters; return isTopLevel && knownPathParamNames.includes(param.name); } const parameters: HttpOperationParameter[] = []; - const resolvedBody = diagnostics.pipe( - resolveBody(program, operation.parameters, metadata, rootPropertyMap, visibility, "request") + const { body: resolvedBody, metadata } = diagnostics.pipe( + resolveHttpPayload(program, operation.parameters, visibility, "request", { + isImplicitPathParam, + }) ); - let contentTypes: string[] | undefined; - for (const param of metadata) { - const queryOptions = getQueryParamOptions(program, param); - const pathOptions = - getPathParamOptions(program, param) ?? - (isImplicitPathParam(param) && { type: "path", name: param.name }); - const headerOptions = getHeaderFieldOptions(program, param); - const isBodyVal = isBody(program, param); - const isBodyRootVal = isBodyRoot(program, param); - const defined = [ - ["query", queryOptions], - ["path", pathOptions], - ["header", headerOptions], - ["body", isBodyVal || isBodyRootVal], - ].filter((x) => !!x[1]); - if (defined.length >= 2) { - diagnostics.add( - createDiagnostic({ - code: "operation-param-duplicate-type", - format: { paramName: param.name, types: defined.map((x) => x[0]).join(", ") }, - target: param, - }) - ); - } - - if (queryOptions) { - parameters.push({ - ...queryOptions, - param, - }); - } else if (pathOptions) { - if (param.optional) { - diagnostics.add( - createDiagnostic({ - code: "optional-path-param", - format: { paramName: param.name }, - target: operation, - }) - ); - } - parameters.push({ - ...pathOptions, - param, - }); - } else if (headerOptions) { - if (isContentTypeHeader(program, param)) { - contentTypes = diagnostics.pipe(getContentTypes(param)); - } - parameters.push({ - ...headerOptions, - param, - }); + for (const item of metadata) { + switch (item.kind) { + case "contentType": + parameters.push({ + name: "Content-Type", + type: "header", + param: item.property, + }); + break; + case "path": + if (item.property.optional) { + diagnostics.add( + createDiagnostic({ + code: "optional-path-param", + format: { paramName: item.property.name }, + target: item.property, + }) + ); + } + // eslint-disable-next-line no-fallthrough + case "query": + case "header": + parameters.push({ + ...item.options, + param: item.property, + }); + break; } } - const body = diagnostics.pipe(computeHttpOperationBody(operation, resolvedBody, contentTypes)); + const body = resolvedBody; return diagnostics.wrap({ parameters, @@ -145,48 +103,7 @@ function getOperationParametersForVerb( return body?.type; }, get bodyParameter() { - return body?.parameter; + return body?.property; }, }); } - -function computeHttpOperationBody( - operation: Operation, - resolvedBody: ResolvedBody | undefined, - contentTypes: string[] | undefined -): [HttpOperationRequestBody | undefined, readonly Diagnostic[]] { - contentTypes ??= []; - const diagnostics: Diagnostic[] = []; - if (resolvedBody === undefined) { - if (contentTypes.length > 0) { - diagnostics.push( - createDiagnostic({ - code: "content-type-ignored", - target: operation.parameters, - }) - ); - } - return [undefined, diagnostics]; - } - - if (contentTypes.includes("multipart/form-data") && resolvedBody.type.kind !== "Model") { - diagnostics.push( - createDiagnostic({ - code: "multipart-model", - target: resolvedBody.property ?? operation.parameters, - }) - ); - return [undefined, diagnostics]; - } - - const body: HttpOperationRequestBody = { - type: resolvedBody.type, - isExplicit: resolvedBody.isExplicit, - containsMetadataAnnotations: resolvedBody.containsMetadataAnnotations, - contentTypes, - }; - if (resolvedBody.property) { - body.parameter = resolvedBody.property; - } - return [body, diagnostics]; -} diff --git a/packages/http/src/payload.ts b/packages/http/src/payload.ts new file mode 100644 index 00000000000..fda7a5536a3 --- /dev/null +++ b/packages/http/src/payload.ts @@ -0,0 +1,491 @@ +import { + Diagnostic, + Model, + ModelProperty, + Program, + Tuple, + Type, + createDiagnosticCollector, + filterModelProperties, + getDiscriminator, + getEncode, + ignoreDiagnostics, + isArrayModelType, + navigateType, +} from "@typespec/compiler"; +import { DuplicateTracker } from "@typespec/compiler/utils"; +import { getContentTypes } from "./content-types.js"; +import { isHeader, isPathParam, isQueryParam, isStatusCode } from "./decorators.js"; +import { + GetHttpPropertyOptions, + HeaderProperty, + HttpProperty, + resolvePayloadProperties, +} from "./http-property.js"; +import { createDiagnostic } from "./lib.js"; +import { Visibility } from "./metadata.js"; +import { HttpFileModel, getHttpFileModel, getHttpPart } from "./private.decorators.js"; +import { HttpOperationBody, HttpOperationMultipartBody, HttpOperationPart } from "./types.js"; + +export interface HttpPayload { + readonly body?: HttpOperationBody | HttpOperationMultipartBody; + readonly metadata: HttpProperty[]; +} +export interface ExtractBodyAndMetadataOptions extends GetHttpPropertyOptions {} +export function resolveHttpPayload( + program: Program, + type: Type, + visibility: Visibility, + usedIn: "request" | "response" | "multipart", + options: ExtractBodyAndMetadataOptions = {} +): [HttpPayload, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + + const metadata = diagnostics.pipe(resolvePayloadProperties(program, type, visibility, options)); + + const body = diagnostics.pipe(resolveBody(program, type, metadata, visibility, usedIn)); + + if (body) { + if ( + body.contentTypes.includes("multipart/form-data") && + body.bodyKind === "single" && + body.type.kind !== "Model" + ) { + diagnostics.add( + createDiagnostic({ + code: "multipart-model", + target: body.property ?? type, + }) + ); + return diagnostics.wrap({ body: undefined, metadata }); + } + } + + return diagnostics.wrap({ body, metadata }); +} + +function resolveBody( + program: Program, + requestOrResponseType: Type, + metadata: HttpProperty[], + visibility: Visibility, + usedIn: "request" | "response" | "multipart" +): [HttpOperationBody | HttpOperationMultipartBody | undefined, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + const resolvedContentTypes = diagnostics.pipe(resolveContentTypes(program, metadata, usedIn)); + + const file = getHttpFileModel(program, requestOrResponseType); + if (file !== undefined) { + const file = getHttpFileModel(program, requestOrResponseType)!; + return diagnostics.wrap({ + bodyKind: "single", + contentTypes: diagnostics.pipe(getContentTypes(file.contentType)), + contentTypeProperty: file.contentType, + type: file.contents.type, + isExplicit: false, + containsMetadataAnnotations: false, + }); + } + + // non-model or intrinsic/array model -> response body is response type + if (requestOrResponseType.kind !== "Model" || isArrayModelType(program, requestOrResponseType)) { + return diagnostics.wrap({ + bodyKind: "single", + ...resolvedContentTypes, + type: requestOrResponseType, + isExplicit: false, + containsMetadataAnnotations: false, + }); + } + + // look for explicit body + const resolvedBody: HttpOperationBody | HttpOperationMultipartBody | undefined = diagnostics.pipe( + resolveExplicitBodyProperty(program, metadata, resolvedContentTypes, visibility, usedIn) + ); + + if (resolvedBody === undefined) { + // Special case if the model as a parent model then we'll return an empty object as this is assumed to be a nominal type. + // Special Case if the model has an indexer then it means it can return props so cannot be void. + if (requestOrResponseType.baseModel || requestOrResponseType.indexer) { + return diagnostics.wrap({ + bodyKind: "single", + ...resolvedContentTypes, + type: requestOrResponseType, + isExplicit: false, + containsMetadataAnnotations: false, + }); + } + // Special case for legacy purposes if the return type is an empty model with only @discriminator("xyz") + // Then we still want to return that object as it technically always has a body with that implicit property. + if ( + requestOrResponseType.derivedModels.length > 0 && + getDiscriminator(program, requestOrResponseType) + ) { + return diagnostics.wrap({ + bodyKind: "single", + ...resolvedContentTypes, + type: requestOrResponseType, + isExplicit: false, + containsMetadataAnnotations: false, + }); + } + } + + const unannotatedProperties = filterModelProperties(program, requestOrResponseType, (p) => + metadata.some((x) => x.property === p && x.kind === "bodyProperty") + ); + + if (unannotatedProperties.properties.size > 0) { + if (resolvedBody === undefined) { + return diagnostics.wrap({ + bodyKind: "single", + ...resolvedContentTypes, + type: unannotatedProperties, + isExplicit: false, + containsMetadataAnnotations: false, + }); + } else { + diagnostics.add( + createDiagnostic({ + code: "duplicate-body", + messageId: "bodyAndUnannotated", + target: requestOrResponseType, + }) + ); + } + } + if (resolvedBody === undefined && resolvedContentTypes.contentTypeProperty) { + diagnostics.add( + createDiagnostic({ + code: "content-type-ignored", + target: resolvedContentTypes.contentTypeProperty, + }) + ); + } + return diagnostics.wrap(resolvedBody); +} + +interface ResolvedContentType { + readonly contentTypes: string[]; + readonly contentTypeProperty?: ModelProperty; +} +function resolveContentTypes( + program: Program, + metadata: HttpProperty[], + usedIn: "request" | "response" | "multipart" +): [ResolvedContentType, readonly Diagnostic[]] { + for (const prop of metadata) { + if (prop.kind === "contentType") { + const [contentTypes, diagnostics] = getContentTypes(prop.property); + return [{ contentTypes, contentTypeProperty: prop.property }, diagnostics]; + } + } + switch (usedIn) { + case "multipart": + // Figure this out later + return [{ contentTypes: [] }, []]; + default: + return [{ contentTypes: ["application/json"] }, []]; + } +} + +function resolveExplicitBodyProperty( + program: Program, + metadata: HttpProperty[], + resolvedContentTypes: ResolvedContentType, + visibility: Visibility, + usedIn: "request" | "response" | "multipart" +): [HttpOperationBody | HttpOperationMultipartBody | undefined, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + let resolvedBody: HttpOperationBody | HttpOperationMultipartBody | undefined; + const duplicateTracker = new DuplicateTracker(); + + for (const item of metadata) { + if (item.kind === "body" || item.kind === "bodyRoot" || item.kind === "multipartBody") { + duplicateTracker.track("body", item.property); + } + + switch (item.kind) { + case "body": + case "bodyRoot": + let containsMetadataAnnotations = false; + if (item.kind === "body") { + const valid = diagnostics.pipe(validateBodyProperty(program, item.property, usedIn)); + containsMetadataAnnotations = !valid; + } + if (resolvedBody === undefined) { + resolvedBody = { + bodyKind: "single", + ...resolvedContentTypes, + type: item.property.type, + isExplicit: item.kind === "body", + containsMetadataAnnotations, + property: item.property, + parameter: item.property, + }; + } + break; + case "multipartBody": + resolvedBody = diagnostics.pipe( + resolveMultiPartBody(program, item.property, resolvedContentTypes, visibility) + ); + break; + } + } + for (const [_, items] of duplicateTracker.entries()) { + for (const prop of items) { + diagnostics.add( + createDiagnostic({ + code: "duplicate-body", + target: prop, + }) + ); + } + } + + return diagnostics.wrap(resolvedBody); +} + +/** Validate a property marked with `@body` */ +function validateBodyProperty( + program: Program, + property: ModelProperty, + usedIn: "request" | "response" | "multipart" +): [boolean, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + navigateType( + property.type, + { + modelProperty: (prop) => { + const kind = isHeader(program, prop) + ? "header" + : (usedIn === "request" || usedIn === "multipart") && isQueryParam(program, prop) + ? "query" + : usedIn === "request" && isPathParam(program, prop) + ? "path" + : usedIn === "response" && isStatusCode(program, prop) + ? "statusCode" + : undefined; + + if (kind) { + diagnostics.add( + createDiagnostic({ + code: "metadata-ignored", + format: { kind }, + target: prop, + }) + ); + } + }, + }, + {} + ); + return diagnostics.wrap(diagnostics.diagnostics.length === 0); +} + +function resolveMultiPartBody( + program: Program, + property: ModelProperty, + resolvedContentTypes: ResolvedContentType, + visibility: Visibility +): [HttpOperationMultipartBody | undefined, readonly Diagnostic[]] { + const type = property.type; + if (type.kind === "Model") { + return resolveMultiPartBodyFromModel(program, property, type, resolvedContentTypes, visibility); + } else if (type.kind === "Tuple") { + return resolveMultiPartBodyFromTuple(program, property, type, resolvedContentTypes, visibility); + } else { + return [undefined, [createDiagnostic({ code: "multipart-model", target: property })]]; + } +} + +function resolveMultiPartBodyFromModel( + program: Program, + property: ModelProperty, + type: Model, + resolvedContentTypes: ResolvedContentType, + visibility: Visibility +): [HttpOperationMultipartBody | undefined, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + const parts: HttpOperationPart[] = []; + for (const item of type.properties.values()) { + const part = diagnostics.pipe(resolvePartOrParts(program, item.type, visibility)); + if (part) { + parts.push({ ...part, name: item.name, optional: item.optional }); + } + } + + return diagnostics.wrap({ + bodyKind: "multipart", + ...resolvedContentTypes, + parts, + property, + type, + }); +} + +const multipartContentTypes = { + formData: "multipart/form-data", + mixed: "multipart/mixed", +} as const; +const multipartContentTypesValues = Object.values(multipartContentTypes); + +function resolveMultiPartBodyFromTuple( + program: Program, + property: ModelProperty, + type: Tuple, + resolvedContentTypes: ResolvedContentType, + visibility: Visibility +): [HttpOperationMultipartBody | undefined, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + const parts: HttpOperationPart[] = []; + + for (const contentType of resolvedContentTypes.contentTypes) { + if (!multipartContentTypesValues.includes(contentType as any)) { + diagnostics.add( + createDiagnostic({ + code: "multipart-invalid-content-type", + format: { contentType, valid: multipartContentTypesValues.join(", ") }, + target: type, + }) + ); + } + } + for (const [index, item] of type.values.entries()) { + const part = diagnostics.pipe(resolvePartOrParts(program, item, visibility)); + if ( + part?.name === undefined && + resolvedContentTypes.contentTypes.includes(multipartContentTypes.formData) + ) { + diagnostics.add( + createDiagnostic({ + code: "formdata-no-part-name", + target: type.node.values[index], + }) + ); + } + if (part) { + parts.push(part); + } + } + + return diagnostics.wrap({ + bodyKind: "multipart", + ...resolvedContentTypes, + parts, + property, + type, + }); +} + +function resolvePartOrParts( + program: Program, + type: Type, + visibility: Visibility +): [HttpOperationPart | undefined, readonly Diagnostic[]] { + if (type.kind === "Model" && isArrayModelType(program, type)) { + const [part, diagnostics] = resolvePart(program, type.indexer.value, visibility); + if (part) { + return [{ ...part, multi: true }, diagnostics]; + } + return [part, diagnostics]; + } else { + return resolvePart(program, type, visibility); + } +} + +function resolvePart( + program: Program, + type: Type, + visibility: Visibility +): [HttpOperationPart | undefined, readonly Diagnostic[]] { + const part = getHttpPart(program, type); + if (part) { + const file = getHttpFileModel(program, part.type); + if (file !== undefined) { + return getFilePart(part.options.name, file); + } + let [{ body, metadata }, diagnostics] = resolveHttpPayload( + program, + part.type, + visibility, + "multipart" + ); + if (body === undefined) { + return [undefined, diagnostics]; + } else if (body.bodyKind === "multipart") { + return [undefined, [createDiagnostic({ code: "multipart-nested", target: type })]]; + } + + if (body.contentTypes.length === 0) { + body = { ...body, contentTypes: resolveDefaultContentTypeForPart(program, body.type) }; + } + return [ + { + multi: false, + name: part.options.name, + body, + optional: false, + headers: metadata.filter((x): x is HeaderProperty => x.kind === "header"), + }, + diagnostics, + ]; + } + return [undefined, [createDiagnostic({ code: "multipart-part", target: type })]]; +} + +function getFilePart( + name: string | undefined, + file: HttpFileModel +): [HttpOperationPart | undefined, readonly Diagnostic[]] { + const [contentTypes, diagnostics] = getContentTypes(file.contentType); + return [ + { + multi: false, + name, + body: { + bodyKind: "single", + contentTypeProperty: file.contentType, + contentTypes: contentTypes, + type: file.contents.type, + isExplicit: false, + containsMetadataAnnotations: false, + }, + filename: file.filename, + optional: false, + headers: [], + }, + diagnostics, + ]; +} + +function resolveDefaultContentTypeForPart(program: Program, type: Type): string[] { + function resolve(type: Type): string[] { + if (type.kind === "Scalar") { + const encodedAs = getEncode(program, type); + if (encodedAs) { + type = encodedAs.type; + } + + if ( + ignoreDiagnostics( + program.checker.isTypeAssignableTo( + type.projectionBase ?? type, + program.checker.getStdType("bytes"), + type + ) + ) + ) { + return ["application/octet-stream"]; + } else { + return ["text/plain"]; + } + } else if (type.kind === "Union") { + return [...type.variants.values()].flatMap((x) => resolve(x.type)); + } else { + return ["application/json"]; + } + } + + return [...new Set(resolve(type))]; +} diff --git a/packages/http/src/private.decorators.ts b/packages/http/src/private.decorators.ts new file mode 100644 index 00000000000..bf2b8ea54ed --- /dev/null +++ b/packages/http/src/private.decorators.ts @@ -0,0 +1,115 @@ +import { + DecoratorContext, + Model, + ModelProperty, + Program, + Type, + getProperty, +} from "@typespec/compiler"; +import { + HttpFileDecorator, + HttpPartDecorator, + PlainDataDecorator, +} from "../generated-defs/TypeSpec.Http.Private.js"; +import { HttpStateKeys } from "./lib.js"; + +export const namespace = "TypeSpec.Http.Private"; + +export const $plainData: PlainDataDecorator = (context: DecoratorContext, entity: Model) => { + const { program } = context; + + const decoratorsToRemove = ["$header", "$body", "$query", "$path", "$statusCode"]; + const [headers, bodies, queries, paths, statusCodes] = [ + program.stateMap(HttpStateKeys.header), + program.stateSet(HttpStateKeys.body), + program.stateMap(HttpStateKeys.query), + program.stateMap(HttpStateKeys.path), + program.stateMap(HttpStateKeys.statusCode), + ]; + + for (const property of entity.properties.values()) { + // Remove the decorators so that they do not run in the future, for example, + // if this model is later spread into another. + property.decorators = property.decorators.filter( + (d) => !decoratorsToRemove.includes(d.decorator.name) + ); + + // Remove the impact the decorators already had on this model. + headers.delete(property); + bodies.delete(property); + queries.delete(property); + paths.delete(property); + statusCodes.delete(property); + } +}; + +export const $httpFile: HttpFileDecorator = (context: DecoratorContext, target: Model) => { + context.program.stateSet(HttpStateKeys.file).add(target); +}; + +/** + * Check if the given type is an `HttpFile` + */ +export function isHttpFile(program: Program, type: Type) { + return program.stateSet(HttpStateKeys.file).has(type); +} + +export function isOrExtendsHttpFile(program: Program, type: Type) { + if (type.kind !== "Model") { + return false; + } + + let current: Model | undefined = type; + + while (current) { + if (isHttpFile(program, current)) { + return true; + } + + current = current.baseModel; + } + + return false; +} + +export interface HttpFileModel { + readonly type: Type; + readonly contentType: ModelProperty; + readonly filename: ModelProperty; + readonly contents: ModelProperty; +} + +export function getHttpFileModel(program: Program, type: Type): HttpFileModel | undefined { + if (type.kind !== "Model" || !isOrExtendsHttpFile(program, type)) { + return undefined; + } + + const contentType = getProperty(type, "contentType")!; + const filename = getProperty(type, "filename")!; + const contents = getProperty(type, "contents")!; + + return { contents, contentType, filename, type }; +} + +export interface HttpPartOptions { + readonly name?: string; +} + +export const $httpPart: HttpPartDecorator = ( + context: DecoratorContext, + target: Model, + type, + options +) => { + context.program.stateMap(HttpStateKeys.httpPart).set(target, { type, options }); +}; + +export interface HttpPart { + readonly type: Type; + readonly options: HttpPartOptions; +} + +/** Return the http part information on a model that is an `HttpPart` */ +export function getHttpPart(program: Program, target: Type): HttpPart | undefined { + return program.stateMap(HttpStateKeys.httpPart).get(target); +} diff --git a/packages/http/src/responses.ts b/packages/http/src/responses.ts index 37d16402c89..6e2d952d556 100644 --- a/packages/http/src/responses.ts +++ b/packages/http/src/responses.ts @@ -14,18 +14,18 @@ import { Program, Type, } from "@typespec/compiler"; -import { resolveBody, ResolvedBody } from "./body.js"; -import { getContentTypes, isContentTypeHeader } from "./content-types.js"; +import { getStatusCodeDescription, getStatusCodesWithDiagnostics } from "./decorators.js"; +import { HttpProperty } from "./http-property.js"; +import { HttpStateKeys, reportDiagnostic } from "./lib.js"; +import { Visibility } from "./metadata.js"; +import { resolveHttpPayload } from "./payload.js"; import { - getHeaderFieldName, - getStatusCodeDescription, - getStatusCodesWithDiagnostics, - isHeader, - isStatusCode, -} from "./decorators.js"; -import { createDiagnostic, HttpStateKeys, reportDiagnostic } from "./lib.js"; -import { gatherMetadata, Visibility } from "./metadata.js"; -import { HttpOperationResponse, HttpStatusCodes, HttpStatusCodesEntry } from "./types.js"; + HttpOperationBody, + HttpOperationMultipartBody, + HttpOperationResponse, + HttpStatusCodes, + HttpStatusCodesEntry, +} from "./types.js"; /** * Get the responses for a given operation. @@ -86,32 +86,18 @@ function processResponseType( responses: ResponseIndex, responseType: Type ) { - const rootPropertyMap = new Map(); - const metadata = gatherMetadata( - program, - diagnostics, - responseType, - Visibility.Read, - undefined, - rootPropertyMap + // Get body + let { body: resolvedBody, metadata } = diagnostics.pipe( + resolveHttpPayload(program, responseType, Visibility.Read, "response") ); - // Get explicity defined status codes const statusCodes: HttpStatusCodes = diagnostics.pipe( getResponseStatusCodes(program, responseType, metadata) ); - // Get explicitly defined content types - const contentTypes = getResponseContentTypes(program, diagnostics, metadata); - // Get response headers const headers = getResponseHeaders(program, metadata); - // Get body - let resolvedBody = diagnostics.pipe( - resolveBody(program, responseType, metadata, rootPropertyMap, Visibility.Read, "response") - ); - // If there is no explicit status code, check if it should be 204 if (statusCodes.length === 0) { if (isErrorModel(program, responseType)) { @@ -127,11 +113,6 @@ function processResponseType( } } - // If there is a body but no explicit content types, use application/json - if (resolvedBody && contentTypes.length === 0) { - contentTypes.push("application/json"); - } - // Put them into currentEndpoint.responses for (const statusCode of statusCodes) { // the first model for this statusCode/content type pair carries the @@ -152,19 +133,9 @@ function processResponseType( if (resolvedBody !== undefined) { response.responses.push({ - body: { - contentTypes: contentTypes, - ...resolvedBody, - }, + body: resolvedBody, headers, }); - } else if (contentTypes.length > 0) { - diagnostics.add( - createDiagnostic({ - code: "content-type-ignored", - target: responseType, - }) - ); } else { response.responses.push({ headers }); } @@ -180,14 +151,14 @@ function processResponseType( function getResponseStatusCodes( program: Program, responseType: Type, - metadata: Set + metadata: HttpProperty[] ): [HttpStatusCodes, readonly Diagnostic[]] { const codes: HttpStatusCodes = []; const diagnostics = createDiagnosticCollector(); let statusFound = false; for (const prop of metadata) { - if (isStatusCode(program, prop)) { + if (prop.kind === "statusCode") { if (statusFound) { reportDiagnostic(program, { code: "multiple-status-codes", @@ -195,7 +166,7 @@ function getResponseStatusCodes( }); } statusFound = true; - codes.push(...diagnostics.pipe(getStatusCodesWithDiagnostics(program, prop))); + codes.push(...diagnostics.pipe(getStatusCodesWithDiagnostics(program, prop.property))); } } @@ -214,37 +185,17 @@ function getExplicitSetStatusCode(program: Program, entity: Model | ModelPropert return program.stateMap(HttpStateKeys.statusCode).get(entity) ?? []; } -/** - * Get explicity defined content-types from response metadata - * Return is an array of strings, possibly empty, which indicates no explicitly defined content-type. - * We do not check for duplicates here -- that will be done by the caller. - */ -function getResponseContentTypes( - program: Program, - diagnostics: DiagnosticCollector, - metadata: Set -): string[] { - const contentTypes: string[] = []; - for (const prop of metadata) { - if (isHeader(program, prop) && isContentTypeHeader(program, prop)) { - contentTypes.push(...diagnostics.pipe(getContentTypes(prop))); - } - } - return contentTypes; -} - /** * Get response headers from response metadata */ function getResponseHeaders( program: Program, - metadata: Set + metadata: HttpProperty[] ): Record { const responseHeaders: Record = {}; for (const prop of metadata) { - const headerName = getHeaderFieldName(program, prop); - if (isHeader(program, prop) && headerName !== "content-type") { - responseHeaders[headerName] = prop; + if (prop.kind === "header") { + responseHeaders[prop.options.name] = prop.property; } } return responseHeaders; @@ -255,7 +206,7 @@ function getResponseDescription( operation: Operation, responseType: Type, statusCode: HttpStatusCodes[number], - body: ResolvedBody | undefined + body: HttpOperationBody | HttpOperationMultipartBody | undefined ): string | undefined { // NOTE: If the response type is an envelope and not the same as the body // type, then use its @doc as the response description. However, if the diff --git a/packages/http/src/types.ts b/packages/http/src/types.ts index e69de12398f..0b3ea86a0d2 100644 --- a/packages/http/src/types.ts +++ b/packages/http/src/types.ts @@ -2,12 +2,15 @@ import { DiagnosticResult, Interface, ListOperationOptions, + Model, ModelProperty, Namespace, Operation, Program, + Tuple, Type, } from "@typespec/compiler"; +import { HeaderProperty } from "./http-property.js"; /** * @deprecated use `HttpOperation`. To remove in November 2022 release. @@ -317,28 +320,18 @@ export type HttpOperationParameter = ( }; /** - * Represent the body information for an http request. - * - * @note the `type` must be a `Model` if the content type is multipart. + * @deprecated use {@link HttpOperationBody} */ -export interface HttpOperationRequestBody extends HttpOperationBody { - /** - * If the body was explicitly set as a property. Correspond to the property with `@body` or `@bodyRoot` - */ - parameter?: ModelProperty; -} - -export interface HttpOperationResponseBody extends HttpOperationBody { - /** - * If the body was explicitly set as a property. Correspond to the property with `@body` or `@bodyRoot` - */ - readonly property?: ModelProperty; -} +export type HttpOperationRequestBody = HttpOperationBody; +/** + * @deprecated use {@link HttpOperationBody} + */ +export type HttpOperationResponseBody = HttpOperationBody; export interface HttpOperationParameters { parameters: HttpOperationParameter[]; - body?: HttpOperationRequestBody; + body?: HttpOperationBody | HttpOperationMultipartBody; /** @deprecated use {@link body.type} */ bodyType?: Type; @@ -446,25 +439,63 @@ export interface HttpOperationResponse { export interface HttpOperationResponseContent { headers?: Record; - body?: HttpOperationResponseBody; + body?: HttpOperationBody | HttpOperationMultipartBody; } -export interface HttpOperationBody { - /** - * Content types. - */ - contentTypes: string[]; +export interface HttpOperationBodyBase { + /** Content types. */ + readonly contentTypes: string[]; + /** Property used to set the content type if exists */ + readonly contentTypeProperty?: ModelProperty; +} - /** - * Type of the operation body. - */ - type: Type; +export interface HttpBody { + readonly type: Type; /** If the body was explicitly set with `@body`. */ readonly isExplicit: boolean; /** If the body contains metadata annotations to ignore. For example `@header`. */ readonly containsMetadataAnnotations: boolean; + + /** + * @deprecated use {@link property} + */ + parameter?: ModelProperty; + + /** + * If the body was explicitly set as a property. Correspond to the property with `@body` or `@bodyRoot` + */ + readonly property?: ModelProperty; +} + +export interface HttpOperationBody extends HttpOperationBodyBase, HttpBody { + readonly bodyKind: "single"; +} + +/** Body marked with `@multipartBody` */ +export interface HttpOperationMultipartBody extends HttpOperationBodyBase { + readonly bodyKind: "multipart"; + readonly type: Model | Tuple; + /** Property annotated with `@multipartBody` */ + readonly property: ModelProperty; + readonly parts: HttpOperationPart[]; +} + +/** Represent an part in a multipart body. */ +export interface HttpOperationPart { + /** Part name */ + readonly name?: string; + /** If the part is optional */ + readonly optional: boolean; + /** Part body */ + readonly body: HttpOperationBody; + /** If the Part is an HttpFile this is the property defining the filename */ + readonly filename?: ModelProperty; + /** Part headers */ + readonly headers: HeaderProperty[]; + /** If there can be multiple of that part */ + readonly multi: boolean; } export interface HttpStatusCodeRange { diff --git a/packages/http/src/validate.ts b/packages/http/src/validate.ts index 0559836dd1e..fe9aa8bafb6 100644 --- a/packages/http/src/validate.ts +++ b/packages/http/src/validate.ts @@ -1,5 +1,5 @@ -import type { Program } from "@typespec/compiler"; -import { reportDiagnostic } from "./lib.js"; +import type { Model, Program } from "@typespec/compiler"; +import { HttpStateKeys, reportDiagnostic } from "./lib.js"; import { getAllHttpServices } from "./operations.js"; import { isSharedRoute } from "./route.js"; import { HttpOperation, HttpService } from "./types.js"; @@ -11,6 +11,32 @@ export function $onValidate(program: Program) { program.reportDiagnostics(diagnostics); } validateSharedRouteConsistency(program, services); + validateHttpFiles(program); +} + +function validateHttpFiles(program: Program) { + const httpFiles = [...program.stateSet(HttpStateKeys.file)]; + + for (const model of httpFiles) { + if (model.kind === "Model") { + validateHttpFileModel(program, model); + } + } +} + +function validateHttpFileModel(program: Program, model: Model) { + for (const prop of model.properties.values()) { + if (prop.name !== "contentType" && prop.name !== "filename" && prop.name !== "contents") { + reportDiagnostic(program, { + code: "http-file-extra-property", + format: { propName: prop.name }, + target: prop, + }); + } + } + for (const child of model.derivedModels) { + validateHttpFileModel(program, child); + } } function groupHttpOperations( @@ -34,6 +60,7 @@ function groupHttpOperations( } return paths; } + function validateSharedRouteConsistency(program: Program, services: HttpService[]) { for (const service of services) { const paths = groupHttpOperations(service.operations); diff --git a/packages/http/test/multipart.test.ts b/packages/http/test/multipart.test.ts new file mode 100644 index 00000000000..19989fd3797 --- /dev/null +++ b/packages/http/test/multipart.test.ts @@ -0,0 +1,249 @@ +import { expectDiagnosticEmpty, expectDiagnostics } from "@typespec/compiler/testing"; +import { deepStrictEqual, ok, strictEqual } from "assert"; +import { describe, it } from "vitest"; +import { HttpOperationMultipartBody } from "../src/types.js"; +import { getOperationsWithServiceNamespace } from "./test-host.js"; + +it("emit diagnostic when using invalid content type for multipart ", async () => { + const diagnostics = await diagnoseHttpOp(` + op read( + @header contentType: "application/json", + @multipartBody body: [HttpPart]): void; + `); + expectDiagnostics(diagnostics, { + code: "@typespec/http/multipart-invalid-content-type", + message: + "Content type 'application/json' is not a multipart content type. Supported content types are: .", + }); +}); + +describe("define with the tuple form", () => { + describe("part without name", () => { + it("emit diagnostic when using multipart/form-data", async () => { + const diagnostics = await diagnoseHttpOp(` + op read( + @header contentType: "multipart/form-data", + @multipartBody body: [HttpPart]): void; + `); + expectDiagnostics(diagnostics, { + code: "@typespec/http/formdata-no-part-name", + message: "Part used in multipart/form-data must have a name.", + }); + }); + + it("include anonymous part when multipart/form-data", async () => { + const body = await getMultipartBody(` + op read( + @header contentType: "multipart/mixed", + @multipartBody body: [HttpPart]): void; + `); + strictEqual(body.parts.length, 1); + strictEqual(body.parts[0].name, undefined); + }); + }); + + it("resolve name from HttpPart options", async () => { + const body = await getMultipartBody(` + op read( + @header contentType: "multipart/mixed", + @multipartBody body: [HttpPart]): void; + `); + strictEqual(body.parts.length, 1); + strictEqual(body.parts[0].name, "myPart"); + }); + + it("using an array of parts marks the part as multi: true", async () => { + const body = await getMultipartBody(` + op read( + @header contentType: "multipart/mixed", + @multipartBody body: [ + HttpPart[] + ]): void; + `); + + strictEqual(body.parts.length, 1); + strictEqual(body.parts[0].multi, true); + }); + + it("emit diagnostic if using non HttpPart in tuple", async () => { + const diagnostics = await diagnoseHttpOp(` + op read( + @header contentType: "multipart/mixed", + @multipartBody body: [string]): void; + `); + expectDiagnostics(diagnostics, { + code: "@typespec/http/multipart-part", + message: "Expect item to be an HttpPart model.", + }); + }); +}); + +describe("define with the object form", () => { + it("use name from property name", async () => { + const body = await getMultipartBody(` + op read( + @header contentType: "multipart/mixed", + @multipartBody body: { + myPropertyPart: HttpPart + }): void; + `); + + strictEqual(body.parts.length, 1); + strictEqual(body.parts[0].name, "myPropertyPart"); + }); + + it("using an array of parts marks the part as multi: true", async () => { + const body = await getMultipartBody(` + op read( + @header contentType: "multipart/mixed", + @multipartBody body: { + part: HttpPart[] + }): void; + `); + + strictEqual(body.parts.length, 1); + strictEqual(body.parts[0].multi, true); + }); + + it("emit diagnostic if using non HttpPart in tuple", async () => { + const diagnostics = await diagnoseHttpOp(` + op read( + @header contentType: "multipart/mixed", + @multipartBody body: { part: string }): void; + `); + expectDiagnostics(diagnostics, { + code: "@typespec/http/multipart-part", + message: "Expect item to be an HttpPart model.", + }); + }); +}); + +describe("resolving part payload", () => { + it("emit diagnostic if trying to use @multipartBody inside an HttpPart", async () => { + const diagnostics = await diagnoseHttpOp(` + op read( + @header contentType: "multipart/mixed", + @multipartBody body: [HttpPart<{@multipartBody nested: []}>]): void; + `); + expectDiagnostics(diagnostics, { + code: "@typespec/http/multipart-nested", + message: "Cannot use @multipartBody inside of an HttpPart", + }); + }); + it("extract headers for the part", async () => { + const op = await getHttpOp(` + op read( + @header contentType: "multipart/mixed", + @header operationHeader: string; + @multipartBody body: [ + HttpPart<{ + @body nested: string, + @header partHeader: string; + }>]): void; + `); + + strictEqual(op.parameters.parameters.length, 2); + strictEqual(op.parameters.parameters[0].name, "Content-Type"); + strictEqual(op.parameters.parameters[1].name, "operation-header"); + + const body = op.parameters.body; + strictEqual(body?.bodyKind, "multipart"); + strictEqual(body.parts.length, 1); + strictEqual(body.parts[0].headers.length, 1); + strictEqual(body.parts[0].headers[0].options.name, "part-header"); + }); + + describe("HttpFile part", () => { + it("emit diagnostic if adding extra properties to File", async () => { + const diagnostics = await diagnoseHttpOp(` + model InvalidFile extends File { + @header extra: string; + } + `); + + expectDiagnostics(diagnostics, { + code: "@typespec/http/http-file-extra-property", + message: "File model cannot define extra properties. Found 'extra'.", + }); + }); + + it("use of HttpFile resolve optional filename", async () => { + const op = await getHttpOp(` + op read( + @header contentType: "multipart/mixed", + @multipartBody body: [ + HttpPart + ]): void; + `); + + const body = op.parameters.body; + strictEqual(body?.bodyKind, "multipart"); + strictEqual(body.parts.length, 1); + ok(body.parts[0].filename, "Filename property should have been resolved"); + strictEqual(body.parts[0].filename.optional, true); + }); + + it("use of custom HttpFile with required filename resolve required", async () => { + const op = await getHttpOp(` + model FileWithFilename extends File { + filename: string; + } + op read( + @header contentType: "multipart/mixed", + @multipartBody body: [ + HttpPart + ]): void; + `); + + const body = op.parameters.body; + strictEqual(body?.bodyKind, "multipart"); + strictEqual(body.parts.length, 1); + ok(body.parts[0].filename, "Filename property should have been resolved"); + strictEqual(body.parts[0].filename.optional, false); + }); + }); + + describe("part default content type", () => { + it.each([ + ["bytes", "application/octet-stream"], + ["File", "*/*"], + ["string", "text/plain"], + ["int32", "text/plain"], + ["string[]", "application/json"], + ["Foo", "application/json", `model Foo { value: string }`], + ])("%s body", async (type, expectedContentType, extra?: string) => { + const body = await getMultipartBody(` + op upload( + @header contentType: "multipart/mixed", + @multipartBody body: [ + HttpPart<${type}>, + HttpPart<${type}>[] + ]): void; + ${extra ?? ""} + `); + + strictEqual(body.parts.length, 2); + deepStrictEqual(body.parts[0].body.contentTypes, [expectedContentType]); + deepStrictEqual(body.parts[1].body.contentTypes, [expectedContentType]); + }); + }); +}); + +async function getHttpOp(code: string) { + const [ops, diagnostics] = await getOperationsWithServiceNamespace(code); + expectDiagnosticEmpty(diagnostics); + strictEqual(ops.length, 1); + return ops[0]; +} + +async function getMultipartBody(code: string): Promise { + const op = await getHttpOp(code); + const body = op.parameters.body; + strictEqual(body?.bodyKind, "multipart"); + return body; +} + +async function diagnoseHttpOp(code: string) { + const [_, diagnostics] = await getOperationsWithServiceNamespace(code); + return diagnostics; +} diff --git a/packages/http/test/responses.test.ts b/packages/http/test/responses.test.ts index bd830ae776e..b8dc963da85 100644 --- a/packages/http/test/responses.test.ts +++ b/packages/http/test/responses.test.ts @@ -84,7 +84,7 @@ it("issues diagnostics for invalid content types", async () => { @route("/test1") @get - op test1(): { @header contentType: string, @body body: Foo }; + op test1(): { @header contentType: int32, @body body: Foo }; @route("/test2") @get op test2(): { @header contentType: 42, @body body: Foo }; @@ -106,15 +106,12 @@ it("supports any casing for string literal 'Content-Type' header properties.", a model Foo {} @route("/test1") - @get op test1(): { @header "content-Type": "text/html", @body body: Foo }; @route("/test2") - @get op test2(): { @header "CONTENT-type": "text/plain", @body body: Foo }; @route("/test3") - @get op test3(): { @header "content-type": "application/json", @body body: Foo }; ` ); diff --git a/packages/http/test/test-host.ts b/packages/http/test/test-host.ts index 0280b7588a5..96268bcffc9 100644 --- a/packages/http/test/test-host.ts +++ b/packages/http/test/test-host.ts @@ -70,7 +70,7 @@ export async function compileOperations( params: { params: r.parameters.parameters.map(({ type, name }) => ({ type, name })), body: - r.parameters.body?.parameter?.name ?? + r.parameters.body?.property?.name ?? (r.parameters.body?.type?.kind === "Model" ? Array.from(r.parameters.body.type.properties.keys()) : undefined), diff --git a/packages/internal-build-utils/CHANGELOG.md b/packages/internal-build-utils/CHANGELOG.md index 61f39a556b7..dc426171af7 100644 --- a/packages/internal-build-utils/CHANGELOG.md +++ b/packages/internal-build-utils/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log - @typespec/internal-build-utils +## 0.57.0 + +### Bump dependencies + +- [#3401](https://github.com/microsoft/typespec/pull/3401) Update dependencies - May 2024 + + ## 0.56.0 ### Bump dependencies diff --git a/packages/internal-build-utils/package.json b/packages/internal-build-utils/package.json index 24e211921b9..21b942d33f2 100644 --- a/packages/internal-build-utils/package.json +++ b/packages/internal-build-utils/package.json @@ -1,6 +1,6 @@ { "name": "@typespec/internal-build-utils", - "version": "0.56.0", + "version": "0.57.0", "author": "Microsoft Corporation", "description": "Internal library to TypeSpec providing helpers to build.", "homepage": "https://typespec.io", diff --git a/packages/json-schema/CHANGELOG.md b/packages/json-schema/CHANGELOG.md index 4257dc4cbdb..25c57cf69e4 100644 --- a/packages/json-schema/CHANGELOG.md +++ b/packages/json-schema/CHANGELOG.md @@ -1,5 +1,22 @@ # Change Log - @typespec/json-schema +## 0.57.0 + +### Bug Fixes + +- [#3398](https://github.com/microsoft/typespec/pull/3398) Fix decorators application for union variants +- [#3022](https://github.com/microsoft/typespec/pull/3022) Update to support new value types +- [#3430](https://github.com/microsoft/typespec/pull/3430) The emitted JSON Schema now doesn't make root schemas for TypeSpec types which do not have the @jsonSchema decorator or are contained in a namespace with that decorator. Instead, such schemas are put into the $defs of any root schema which references them, and are referenced using JSON Pointers. + +### Bump dependencies + +- [#3401](https://github.com/microsoft/typespec/pull/3401) Update dependencies - May 2024 + +### Features + +- [#3557](https://github.com/microsoft/typespec/pull/3557) Add support for @oneOf decorator. + + ## 0.56.0 ### Bump dependencies diff --git a/packages/json-schema/README.md b/packages/json-schema/README.md index 9abdc9d89e5..17a93bfaaa9 100644 --- a/packages/json-schema/README.md +++ b/packages/json-schema/README.md @@ -95,6 +95,7 @@ When true, emit all references as json schema files, even if the referenced type - [`@minContains`](#@mincontains) - [`@minProperties`](#@minproperties) - [`@multipleOf`](#@multipleof) +- [`@oneOf`](#@oneof) - [`@prefixItems`](#@prefixitems) - [`@uniqueItems`](#@uniqueitems) @@ -348,6 +349,22 @@ Specify that the numeric type must be a multiple of some numeric value. | ----- | ----------------- | -------------------------------------------------- | | value | `valueof numeric` | The numeric type must be a multiple of this value. | +#### `@oneOf` + +Specify that `oneOf` should be used instead of `anyOf` for that union. + +```typespec +@TypeSpec.JsonSchema.oneOf +``` + +##### Target + +`Union | ModelProperty` + +##### Parameters + +None + #### `@prefixItems` Specify that the target array must begin with the provided types. diff --git a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts index b65738bc4aa..e33657fcc02 100644 --- a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts +++ b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts @@ -5,6 +5,7 @@ import type { Numeric, Scalar, Type, + Union, } from "@typespec/compiler"; /** @@ -43,6 +44,11 @@ export type BaseUriDecorator = ( */ export type IdDecorator = (context: DecoratorContext, target: Type, id: string) => void; +/** + * Specify that `oneOf` should be used instead of `anyOf` for that union. + */ +export type OneOfDecorator = (context: DecoratorContext, target: Union | ModelProperty) => void; + /** * Specify that the numeric type must be a multiple of some numeric value. * diff --git a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts-test.ts b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts-test.ts index 8e2778f71f1..80ecdef7f8e 100644 --- a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts-test.ts +++ b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts-test.ts @@ -13,6 +13,7 @@ import { $minContains, $minProperties, $multipleOf, + $oneOf, $prefixItems, $uniqueItems, } from "@typespec/json-schema"; @@ -30,6 +31,7 @@ import type { MinContainsDecorator, MinPropertiesDecorator, MultipleOfDecorator, + OneOfDecorator, PrefixItemsDecorator, UniqueItemsDecorator, } from "./TypeSpec.JsonSchema.js"; @@ -38,6 +40,7 @@ type Decorators = { $jsonSchema: JsonSchemaDecorator; $baseUri: BaseUriDecorator; $id: IdDecorator; + $oneOf: OneOfDecorator; $multipleOf: MultipleOfDecorator; $contains: ContainsDecorator; $minContains: MinContainsDecorator; @@ -57,6 +60,7 @@ const _: Decorators = { $jsonSchema, $baseUri, $id, + $oneOf, $multipleOf, $contains, $minContains, diff --git a/packages/json-schema/lib/main.tsp b/packages/json-schema/lib/main.tsp index fae17c91d93..3aa30c31f0d 100644 --- a/packages/json-schema/lib/main.tsp +++ b/packages/json-schema/lib/main.tsp @@ -30,6 +30,11 @@ extern dec baseUri(target: Reflection.Namespace, baseUri: valueof string); */ extern dec id(target: unknown, id: valueof string); +/** + * Specify that `oneOf` should be used instead of `anyOf` for that union. + */ +extern dec oneOf(target: Reflection.Union | Reflection.ModelProperty); + /** * Specify that the numeric type must be a multiple of some numeric value. * diff --git a/packages/json-schema/package.json b/packages/json-schema/package.json index 688bd2e94e3..85b96e40fa7 100644 --- a/packages/json-schema/package.json +++ b/packages/json-schema/package.json @@ -1,6 +1,6 @@ { "name": "@typespec/json-schema", - "version": "0.56.0", + "version": "0.57.0", "author": "Microsoft Corporation", "description": "TypeSpec library for emitting TypeSpec to JSON Schema and converting JSON Schema to TypeSpec", "homepage": "https://github.com/microsoft/typespec", diff --git a/packages/json-schema/src/index.ts b/packages/json-schema/src/index.ts index de5c4bdced7..27a23b8defc 100644 --- a/packages/json-schema/src/index.ts +++ b/packages/json-schema/src/index.ts @@ -29,6 +29,7 @@ import { MinContainsDecorator, MinPropertiesDecorator, MultipleOfDecorator, + OneOfDecorator, PrefixItemsDecorator, UniqueItemsDecorator, } from "../generated-defs/TypeSpec.JsonSchema.js"; @@ -145,6 +146,15 @@ export function getId(program: Program, target: Type) { return program.stateMap(idKey).get(target); } +const oneOfKey = createStateSymbol("JsonSchema.oneOf"); +export const $oneOf: OneOfDecorator = (context: DecoratorContext, target: Type) => { + context.program.stateMap(oneOfKey).set(target, true); +}; + +export function isOneOf(program: Program, target: Type) { + return program.stateMap(oneOfKey).has(target); +} + const containsKey = createStateSymbol("JsonSchema.contains"); export const $contains: ContainsDecorator = ( context: DecoratorContext, diff --git a/packages/json-schema/src/json-schema-emitter.ts b/packages/json-schema/src/json-schema-emitter.ts index 36da5fc205b..ee99b954df6 100644 --- a/packages/json-schema/src/json-schema-emitter.ts +++ b/packages/json-schema/src/json-schema-emitter.ts @@ -71,6 +71,7 @@ import { getPrefixItems, getUniqueItems, isJsonSchemaDeclaration, + isOneOf, } from "./index.js"; import { JSONSchemaEmitterOptions, reportDiagnostic } from "./lib.js"; export class JsonSchemaEmitter extends TypeEmitter, JSONSchemaEmitterOptions> { @@ -174,6 +175,11 @@ export class JsonSchemaEmitter extends TypeEmitter, JSONSche result.default = this.#getDefaultValue(property.type, property.default); } + if (result.anyOf && isOneOf(this.emitter.getProgram(), property)) { + result.oneOf = result.anyOf; + delete result.anyOf; + } + this.#applyConstraints(property, result); return result; @@ -296,8 +302,10 @@ export class JsonSchemaEmitter extends TypeEmitter, JSONSche } unionDeclaration(union: Union, name: string): EmitterOutput { + const key = isOneOf(this.emitter.getProgram(), union) ? "oneOf" : "anyOf"; + const withConstraints = this.#initializeSchema(union, name, { - anyOf: this.emitter.emitUnionVariants(union), + [key]: this.emitter.emitUnionVariants(union), }); this.#applyConstraints(union, withConstraints); @@ -305,8 +313,10 @@ export class JsonSchemaEmitter extends TypeEmitter, JSONSche } unionLiteral(union: Union): EmitterOutput { + const key = isOneOf(this.emitter.getProgram(), union) ? "oneOf" : "anyOf"; + return new ObjectBuilder({ - anyOf: this.emitter.emitUnionVariants(union), + [key]: this.emitter.emitUnionVariants(union), }); } diff --git a/packages/json-schema/test/unions.test.ts b/packages/json-schema/test/unions.test.ts index 34256aca1bb..e022be0ecd4 100644 --- a/packages/json-schema/test/unions.test.ts +++ b/packages/json-schema/test/unions.test.ts @@ -88,6 +88,26 @@ describe("emitting unions", () => { assert.strictEqual(Foo["x-foo"], true); }); + it("handles oneOf decorator", async () => { + const schemas = await emitSchema(` + @oneOf + union Foo { + "a", + "b" + } + + model Bar { + @oneOf + prop: "a" | "b" + } + `); + + const Foo = schemas["Foo.json"]; + const Bar = schemas["Bar.json"]; + + assert.ok(Foo.oneOf, "Foo uses oneOf"); + assert.ok(Bar.properties.prop.oneOf, "Bar.prop uses oneOf"); + }); it("handles decorators on variants", async () => { const schemas = await emitSchema(` union Foo { diff --git a/packages/library-linter/CHANGELOG.md b/packages/library-linter/CHANGELOG.md index b7b9aa3c380..f7cd7fa796d 100644 --- a/packages/library-linter/CHANGELOG.md +++ b/packages/library-linter/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log - @typespec/library-linter +## 0.57.0 + +### Bump dependencies + +- [#3401](https://github.com/microsoft/typespec/pull/3401) Update dependencies - May 2024 + + ## 0.56.0 ### Bump dependencies diff --git a/packages/library-linter/package.json b/packages/library-linter/package.json index c7647c34eb0..32471e0fecc 100644 --- a/packages/library-linter/package.json +++ b/packages/library-linter/package.json @@ -1,6 +1,6 @@ { "name": "@typespec/library-linter", - "version": "0.56.0", + "version": "0.57.0", "author": "Microsoft Corporation", "description": "TypeSpec library for linting another library.", "homepage": "https://typespec.io", diff --git a/packages/openapi/CHANGELOG.md b/packages/openapi/CHANGELOG.md index 4f571b03c8b..be8d63cbd9d 100644 --- a/packages/openapi/CHANGELOG.md +++ b/packages/openapi/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log - @typespec/openapi +## 0.57.0 + +### Bump dependencies + +- [#3401](https://github.com/microsoft/typespec/pull/3401) Update dependencies - May 2024 + + ## 0.56.0 ### Bump dependencies diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 05bc0035fcd..1cea77790b5 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -1,6 +1,6 @@ { "name": "@typespec/openapi", - "version": "0.56.0", + "version": "0.57.0", "author": "Microsoft Corporation", "description": "TypeSpec library providing OpenAPI concepts", "homepage": "https://typespec.io", diff --git a/packages/openapi3/CHANGELOG.md b/packages/openapi3/CHANGELOG.md index 7f11a5d0732..3766d08fe7e 100644 --- a/packages/openapi3/CHANGELOG.md +++ b/packages/openapi3/CHANGELOG.md @@ -1,5 +1,21 @@ # Change Log - @typespec/openapi3 +## 0.57.0 + +### Bug Fixes + +- [#3342](https://github.com/microsoft/typespec/pull/3342) Add support for new multipart constructs in http library +- [#3574](https://github.com/microsoft/typespec/pull/3574) Emit diagnostic when an invalid type is used as a property instead of crashing. + +### Bump dependencies + +- [#3401](https://github.com/microsoft/typespec/pull/3401) Update dependencies - May 2024 + +### Features + +- [#3022](https://github.com/microsoft/typespec/pull/3022) Add support for new object and array values as default values (e.g. `decimals: decimal[] = #[123, 456.7];`) + + ## 0.56.0 ### Bug Fixes diff --git a/packages/openapi3/package.json b/packages/openapi3/package.json index 993f709cbb2..9b30c996a4d 100644 --- a/packages/openapi3/package.json +++ b/packages/openapi3/package.json @@ -1,6 +1,6 @@ { "name": "@typespec/openapi3", - "version": "0.56.0", + "version": "0.57.0", "author": "Microsoft Corporation", "description": "TypeSpec library for emitting OpenAPI 3.0 from the TypeSpec REST protocol binding", "homepage": "https://typespec.io", diff --git a/packages/openapi3/src/lib.ts b/packages/openapi3/src/lib.ts index 8b45badd1ca..64ef47e2676 100644 --- a/packages/openapi3/src/lib.ts +++ b/packages/openapi3/src/lib.ts @@ -245,6 +245,12 @@ export const libDef = { default: paramMessage`Status code range '${"start"} to '${"end"}' is not supported. OpenAPI 3.0 can only represent range 1XX, 2XX, 3XX, 4XX and 5XX. Example: \`@minValue(400) @maxValue(499)\` for 4XX.`, }, }, + "invalid-model-property": { + severity: "error", + messages: { + default: paramMessage`'${"type"}' cannot be specified as a model property.`, + }, + }, "unsupported-auth": { severity: "warning", messages: { diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index 4cf5c04e4fc..6922ab3aef3 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -60,9 +60,11 @@ import { HeaderFieldOptions, HttpAuth, HttpOperation, + HttpOperationBody, + HttpOperationMultipartBody, HttpOperationParameter, HttpOperationParameters, - HttpOperationRequestBody, + HttpOperationPart, HttpOperationResponse, HttpOperationResponseContent, HttpServer, @@ -70,6 +72,7 @@ import { HttpStatusCodesEntry, HttpVerb, isContentTypeHeader, + isOrExtendsHttpFile, isOverloadSameEndpoint, MetadataInfo, QueryParameterOptions, @@ -93,10 +96,12 @@ import { buildVersionProjections, VersionProjections } from "@typespec/versionin import { stringify } from "yaml"; import { getRef } from "./decorators.js"; import { createDiagnostic, FileType, OpenAPI3EmitterOptions } from "./lib.js"; -import { getDefaultValue, OpenAPI3SchemaEmitter } from "./schema-emitter.js"; +import { getDefaultValue, isBytesKeptRaw, OpenAPI3SchemaEmitter } from "./schema-emitter.js"; import { OpenAPI3Document, + OpenAPI3Encoding, OpenAPI3Header, + OpenAPI3MediaType, OpenAPI3OAuthFlows, OpenAPI3Operation, OpenAPI3Parameter, @@ -527,8 +532,8 @@ function createOAPIEmitter( /** * Validates that common bodies are consistent and returns the minimal set that describes the differences. */ - function validateCommonBodies(ops: HttpOperation[]): HttpOperationRequestBody[] | undefined { - const allBodies = ops.map((op) => op.parameters.body) as HttpOperationRequestBody[]; + function validateCommonBodies(ops: HttpOperation[]): HttpOperationBody[] | undefined { + const allBodies = ops.map((op) => op.parameters.body) as HttpOperationBody[]; return [...new Set(allBodies)]; } @@ -588,7 +593,7 @@ function createOAPIEmitter( summary: string | undefined; verb: HttpVerb; parameters: HttpOperationParameters; - bodies: HttpOperationRequestBody[] | undefined; + bodies: HttpOperationBody[] | undefined; authentication?: Authentication; responses: Map; operations: Operation[]; @@ -692,7 +697,7 @@ function createOAPIEmitter( operations: operations.map((op) => op.operation), parameters: { parameters: [], - }, + } as any, bodies: undefined, authentication: operations[0].authentication, responses: new Map(), @@ -1016,15 +1021,7 @@ function createOAPIEmitter( } obj.content ??= {}; for (const contentType of data.body.contentTypes) { - const isBinary = isBinaryPayload(data.body.type, contentType); - const schema = isBinary - ? { type: "string", format: "binary" } - : getSchemaForBody( - data.body.type, - Visibility.Read, - data.body.isExplicit && data.body.containsMetadataAnnotations, - undefined - ); + const { schema } = getBodyContentEntry(data.body, Visibility.Read, contentType); if (schemaMap.has(contentType)) { schemaMap.get(contentType)!.push(schema); } else { @@ -1127,7 +1124,32 @@ function createOAPIEmitter( }) as any; } - function getSchemaForBody( + function getBodyContentEntry( + body: HttpOperationBody | HttpOperationMultipartBody, + visibility: Visibility, + contentType: string + ): OpenAPI3MediaType { + const isBinary = isBinaryPayload(body.type, contentType); + if (isBinary) { + return { schema: { type: "string", format: "binary" } }; + } + + switch (body.bodyKind) { + case "single": + return { + schema: getSchemaForSingleBody( + body.type, + visibility, + body.isExplicit && body.containsMetadataAnnotations, + contentType.startsWith("multipart/") ? contentType : undefined + ), + }; + case "multipart": + return getBodyContentForMultipartBody(body, visibility, contentType); + } + } + + function getSchemaForSingleBody( type: Type, visibility: Visibility, ignoreMetadataAnnotations: boolean, @@ -1142,6 +1164,119 @@ function createOAPIEmitter( ); } + function getBodyContentForMultipartBody( + body: HttpOperationMultipartBody, + visibility: Visibility, + contentType: string + ): OpenAPI3MediaType { + const properties: Record = {}; + const requiredProperties: string[] = []; + const encodings: Record = {}; + for (const [partIndex, part] of body.parts.entries()) { + const partName = part.name ?? `part${partIndex}`; + let schema = isBytesKeptRaw(program, part.body.type) + ? { type: "string", format: "binary" } + : getSchemaForSingleBody( + part.body.type, + visibility, + part.body.isExplicit && part.body.containsMetadataAnnotations, + part.body.type.kind === "Union" ? contentType : undefined + ); + + if (part.multi) { + schema = { + type: "array", + items: schema, + }; + } + properties[partName] = schema; + + const encoding = resolveEncodingForMultipartPart(part, visibility, schema); + if (encoding) { + encodings[partName] = encoding; + } + if (!part.optional) { + requiredProperties.push(partName); + } + } + + const schema: OpenAPI3Schema = { + type: "object", + properties, + required: requiredProperties, + }; + + const name = + "name" in body.type && body.type.name !== "" + ? getOpenAPITypeName(program, body.type, typeNameOptions) + : undefined; + if (name) { + root.components!.schemas![name] = schema; + } + const result: OpenAPI3MediaType = { + schema: name ? { $ref: "#/components/schemas/" + name } : schema, + }; + + if (Object.keys(encodings).length > 0) { + result.encoding = encodings; + } + return result; + } + + function resolveEncodingForMultipartPart( + part: HttpOperationPart, + visibility: Visibility, + schema: OpenAPI3Schema + ): OpenAPI3Encoding | undefined { + const encoding: OpenAPI3Encoding = {}; + if (!isDefaultContentTypeForOpenAPI3(part.body.contentTypes, schema)) { + encoding.contentType = part.body.contentTypes.join(", "); + } + const headers = part.headers; + if (headers.length > 0) { + encoding.headers = {}; + for (const header of headers) { + const schema = getOpenAPIParameterBase(header.property, visibility); + if (schema !== undefined) { + encoding.headers[header.options.name] = schema; + } + } + } + if (Object.keys(encoding).length === 0) { + return undefined; + } + return encoding; + } + + function isDefaultContentTypeForOpenAPI3( + contentTypes: string[], + schema: OpenAPI3Schema + ): boolean { + if (contentTypes.length === 0) { + return false; + } + if (contentTypes.length > 1) { + return false; + } + const contentType = contentTypes[0]; + + switch (contentType) { + case "text/plain": + return schema.type === "string" || schema.type === "number"; + case "application/octet-stream": + return ( + (schema.type === "string" && schema.format === "binary") || + (schema.type === "array" && + (schema.items as any)?.type === "string" && + (schema.items as any)?.format === "binary") + ); + case "application/json": + return schema.type === "object"; + } + + return false; + } + function getParamPlaceholder(property: ModelProperty) { let spreadParam = false; @@ -1190,10 +1325,7 @@ function createOAPIEmitter( } } - function emitMergedRequestBody( - bodies: HttpOperationRequestBody[] | undefined, - visibility: Visibility - ) { + function emitMergedRequestBody(bodies: HttpOperationBody[] | undefined, visibility: Visibility) { if (bodies === undefined) { return; } @@ -1203,7 +1335,7 @@ function createOAPIEmitter( }; const schemaMap = new Map(); for (const body of bodies) { - const desc = body.parameter ? getDoc(program, body.parameter) : undefined; + const desc = body.property ? getDoc(program, body.property) : undefined; if (desc) { requestBody.description = requestBody.description ? `${requestBody.description} ${desc}` @@ -1211,15 +1343,7 @@ function createOAPIEmitter( } const contentTypes = body.contentTypes.length > 0 ? body.contentTypes : ["application/json"]; for (const contentType of contentTypes) { - const isBinary = isBinaryPayload(body.type, contentType); - const bodySchema = isBinary - ? { type: "string", format: "binary" } - : getSchemaForBody( - body.type, - visibility, - body.isExplicit, - contentType.startsWith("multipart/") ? contentType : undefined - ); + const { schema: bodySchema } = getBodyContentEntry(body, visibility, contentType); if (schemaMap.has(contentType)) { schemaMap.get(contentType)!.push(bodySchema); } else { @@ -1241,32 +1365,23 @@ function createOAPIEmitter( currentEndpoint.requestBody = requestBody; } - function emitRequestBody(body: HttpOperationRequestBody | undefined, visibility: Visibility) { + function emitRequestBody( + body: HttpOperationBody | HttpOperationMultipartBody | undefined, + visibility: Visibility + ) { if (body === undefined || isVoidType(body.type)) { return; } const requestBody: any = { - description: body.parameter ? getDoc(program, body.parameter) : undefined, - required: body.parameter ? !body.parameter.optional : true, + description: body.property ? getDoc(program, body.property) : undefined, + required: body.property ? !body.property.optional : true, content: {}, }; const contentTypes = body.contentTypes.length > 0 ? body.contentTypes : ["application/json"]; for (const contentType of contentTypes) { - const isBinary = isBinaryPayload(body.type, contentType); - const bodySchema = isBinary - ? { type: "string", format: "binary" } - : getSchemaForBody( - body.type, - visibility, - body.isExplicit && body.containsMetadataAnnotations, - contentType.startsWith("multipart/") ? contentType : undefined - ); - const contentEntry: any = { - schema: bodySchema, - }; - requestBody.content[contentType] = contentEntry; + requestBody.content[contentType] = getBodyContentEntry(body, visibility, contentType); } currentEndpoint.requestBody = requestBody; @@ -1468,6 +1583,9 @@ function createOAPIEmitter( function processUnreferencedSchemas() { const addSchema = (type: Type) => { + if (isOrExtendsHttpFile(program, type)) { + return; + } if ( visibilityUsage.isUnreachable(type) && !paramModels.has(type) && diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index 3413b284c1f..3e05cd9978f 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -325,24 +325,17 @@ export class OpenAPI3SchemaEmitter extends TypeEmitter< return props; } - #isBytesKeptRaw(type: Type) { - const program = this.emitter.getProgram(); - return ( - type.kind === "Scalar" && type.name === "bytes" && getEncode(program, type) === undefined - ); - } - modelPropertyLiteral(prop: ModelProperty): EmitterOutput { const program = this.emitter.getProgram(); const isMultipart = this.#getContentType().startsWith("multipart/"); if (isMultipart) { - if (this.#isBytesKeptRaw(prop.type) && getEncode(program, prop) === undefined) { + if (isBytesKeptRaw(program, prop.type) && getEncode(program, prop) === undefined) { return { type: "string", format: "binary" }; } if ( prop.type.kind === "Model" && isArrayModelType(program, prop.type) && - this.#isBytesKeptRaw(prop.type.indexer.value) + isBytesKeptRaw(program, prop.type.indexer.value) ) { return { type: "array", items: { type: "string", format: "binary" } }; } @@ -352,13 +345,20 @@ export class OpenAPI3SchemaEmitter extends TypeEmitter< referenceContext: isMultipart && (prop.type.kind !== "Union" || - ![...prop.type.variants.values()].some((x) => this.#isBytesKeptRaw(x.type))) + ![...prop.type.variants.values()].some((x) => isBytesKeptRaw(program, x.type))) ? { contentType: "application/json" } : {}, }); if (refSchema.kind !== "code") { - throw new Error("Unexpected non-code result from emit reference"); + reportDiagnostic(program, { + code: "invalid-model-property", + format: { + type: prop.type.kind, + }, + target: prop, + }); + return {}; } const isRef = refSchema.value instanceof Placeholder || "$ref" in refSchema.value; @@ -494,7 +494,7 @@ export class OpenAPI3SchemaEmitter extends TypeEmitter< continue; } - if (isMultipart && this.#isBytesKeptRaw(variant.type)) { + if (isMultipart && isBytesKeptRaw(program, variant.type)) { schemaMembers.push({ schema: { type: "string", format: "binary" }, type: variant.type }); continue; } @@ -1012,3 +1012,7 @@ export function getDefaultValue(program: Program, defaultType: Value): any { }); } } + +export function isBytesKeptRaw(program: Program, type: Type) { + return type.kind === "Scalar" && type.name === "bytes" && getEncode(program, type) === undefined; +} diff --git a/packages/openapi3/test/multipart.test.ts b/packages/openapi3/test/multipart.test.ts index 9301cb71d17..c7f8728b574 100644 --- a/packages/openapi3/test/multipart.test.ts +++ b/packages/openapi3/test/multipart.test.ts @@ -1,8 +1,216 @@ import { deepStrictEqual } from "assert"; -import { describe, it } from "vitest"; +import { describe, expect, it } from "vitest"; +import { OpenAPI3Encoding, OpenAPI3Schema } from "../src/types.js"; import { openApiFor } from "./test-host.js"; -describe("typespec-autorest: multipart", () => { +it("create dedicated model for multipart", async () => { + const res = await openApiFor( + ` + model Form { name: HttpPart, profileImage: HttpPart } + op upload(@header contentType: "multipart/form-data", @multipartBody body: Form): void; + ` + ); + const op = res.paths["/"].post; + deepStrictEqual(op.requestBody.content["multipart/form-data"], { + schema: { + $ref: "#/components/schemas/Form", + }, + }); +}); + +it("part of type `bytes` produce `type: string, format: binary`", async () => { + const res = await openApiFor( + ` + op upload(@header contentType: "multipart/form-data", @multipartBody body: { profileImage: HttpPart }): void; + ` + ); + const op = res.paths["/"].post; + deepStrictEqual(op.requestBody.content["multipart/form-data"], { + schema: { + type: "object", + properties: { + profileImage: { + format: "binary", + type: "string", + }, + }, + required: ["profileImage"], + }, + }); +}); + +it("part of type union `HttpPart` produce `type: string, format: binary`", async () => { + const res = await openApiFor( + ` + op upload(@header contentType: "multipart/form-data", @multipartBody _: {profileImage: HttpPart}): void; + ` + ); + const op = res.paths["/"].post; + deepStrictEqual(op.requestBody.content["multipart/form-data"], { + schema: { + type: "object", + properties: { + profileImage: { + anyOf: [ + { + type: "string", + format: "binary", + }, + { + type: "object", + properties: { + content: { type: "string", format: "byte" }, + }, + required: ["content"], + }, + ], + }, + }, + required: ["profileImage"], + }, + encoding: { + profileImage: { + contentType: "application/octet-stream, application/json", + }, + }, + }); +}); + +it("part of type `bytes[]` produce `type: array, items: {type: string, format: binary}`", async () => { + const res = await openApiFor( + ` + op upload(@header contentType: "multipart/form-data", @multipartBody _: { profileImage: HttpPart[]}): void; + ` + ); + const op = res.paths["/"].post; + deepStrictEqual(op.requestBody.content["multipart/form-data"], { + schema: { + type: "object", + properties: { + profileImage: { + type: "array", + items: { type: "string", format: "binary" }, + }, + }, + required: ["profileImage"], + }, + }); +}); + +it("part of type `string` produce `type: string`", async () => { + const res = await openApiFor( + ` + op upload(@header contentType: "multipart/form-data", @multipartBody _: { name: HttpPart }): void; + ` + ); + const op = res.paths["/"].post; + deepStrictEqual(op.requestBody.content["multipart/form-data"], { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + required: ["name"], + }, + }); +}); + +it("part of type `object` produce an object", async () => { + const res = await openApiFor( + ` + op upload(@header contentType: "multipart/form-data", @multipartBody _: { address: HttpPart<{city: string, street: string}>}): void; + ` + ); + const op = res.paths["/"].post; + deepStrictEqual(op.requestBody.content["multipart/form-data"], { + schema: { + type: "object", + properties: { + address: { + type: "object", + properties: { + city: { + type: "string", + }, + street: { + type: "string", + }, + }, + required: ["city", "street"], + }, + }, + required: ["address"], + }, + }); +}); + +it("bytes inside a json part will be treated as base64 encoded by default(same as for a json body)", async () => { + const res = await openApiFor( + ` + op upload(@header contentType: "multipart/form-data", @multipartBody _: { address: HttpPart<{city: string, icon: bytes}> }): void; + ` + ); + const op = res.paths["/"].post; + deepStrictEqual( + op.requestBody.content["multipart/form-data"].schema.properties.address.properties.icon, + { + type: "string", + format: "byte", + } + ); +}); + +describe("part mapping", () => { + it.each([ + [`string`, { type: "string" }], + [`bytes`, { type: "string", format: "binary" }], + [`string[]`, { type: "array", items: { type: "string" } }, { contentType: "application/json" }], + [ + `bytes[]`, + { type: "array", items: { type: "string", format: "byte" } }, + { contentType: "application/json" }, + ], + [ + `{@header contentType: "image/png", @body image: bytes}`, + { type: "string", format: "binary" }, + { contentType: "image/png" }, + ], + [`File`, { type: "string", format: "binary" }, { contentType: "*/*" }], + [ + `{@header extra: string, @body body: string}`, + { type: "string" }, + { + headers: { + extra: { + required: true, + schema: { + type: "string", + }, + }, + }, + }, + ], + ] satisfies [string, OpenAPI3Schema, OpenAPI3Encoding?][])( + `HttpPart<%s>`, + async (type: string, expectedSchema: OpenAPI3Schema, expectedEncoding?: OpenAPI3Encoding) => { + const res = await openApiFor( + ` + op upload(@header contentType: "multipart/form-data", @multipartBody _: { part: HttpPart<${type}> }): void; + ` + ); + const content = res.paths["/"].post.requestBody.content["multipart/form-data"]; + expect(content.schema.properties.part).toEqual(expectedSchema); + + if (expectedEncoding || content.encoding?.part) { + expect(content.encoding?.part).toEqual(expectedEncoding); + } + } + ); +}); + +describe("legacy implicit form", () => { it("add MultiPart suffix to model name", async () => { const res = await openApiFor( ` diff --git a/packages/playground-website/e2e/ui.e2e.ts b/packages/playground-website/e2e/ui.e2e.ts index 20d3359477c..f3b1668f6b9 100644 --- a/packages/playground-website/e2e/ui.e2e.ts +++ b/packages/playground-website/e2e/ui.e2e.ts @@ -10,15 +10,24 @@ test.describe("playground UI tests", () => { await page.goto(host); const samplesDropDown = page.locator("_react=SamplesDropdown").locator("select"); await samplesDropDown.selectOption({ label: "HTTP service" }); - const outputContainer = page.locator("_react=OutputContent"); + const outputContainer = page.locator("_react=FileOutput"); await expect(outputContainer).toContainText(`title: Widget Service`); }); + test("report compilation errors", async ({ page }) => { + await page.goto(host); + const typespecEditorContainer = page.locator("_react=TypeSpecEditor"); + await typespecEditorContainer.click(); + await typespecEditorContainer.pressSequentially("invalid"); + const outputContainer = page.locator("_react=OutputView"); + await expect(outputContainer).toContainText(`No files emitted.`); + }); + test("shared link works", async ({ page }) => { // Pass code "op sharedCode(): string;" // cspell:disable-next-line await page.goto(`${host}/?c=b3Agc2hhcmVkQ29kZSgpOiBzdHJpbmc7`); - const outputContainer = page.locator("_react=OutputContent"); + const outputContainer = page.locator("_react=FileOutput"); await expect(outputContainer).toContainText(`operationId: sharedCode`); }); diff --git a/packages/playground/.storybook/main.ts b/packages/playground/.storybook/main.ts index 62eb9d9c260..9735f6fab4a 100644 --- a/packages/playground/.storybook/main.ts +++ b/packages/playground/.storybook/main.ts @@ -1,8 +1,15 @@ -import type { StorybookConfig } from "@storybook/react-vite"; +import type { FrameworkOptions, StorybookConfig } from "@storybook/react-vite"; const config: StorybookConfig = { stories: ["../stories/**/*.stories.@(ts|tsx)"], - framework: "@storybook/react-vite", + framework: { + name: "@storybook/react-vite", + options: { + builder: { + viteConfigPath: "vite.storybook.config.ts", + }, + } satisfies FrameworkOptions, + }, typescript: { reactDocgen: false, }, diff --git a/packages/playground/.storybook/preview-head.html b/packages/playground/.storybook/preview-head.html new file mode 100644 index 00000000000..1752c3563cf --- /dev/null +++ b/packages/playground/.storybook/preview-head.html @@ -0,0 +1,11 @@ + + diff --git a/packages/playground/CHANGELOG.md b/packages/playground/CHANGELOG.md index 954079fd492..486437fba4a 100644 --- a/packages/playground/CHANGELOG.md +++ b/packages/playground/CHANGELOG.md @@ -1,5 +1,21 @@ # Change Log - @typespec/playground +## 0.3.0 + +### Bug Fixes + +- [#3542](https://github.com/microsoft/typespec/pull/3542) Fix issue where hover tooltip would be cropped or not visible + +### Bump dependencies + +- [#3401](https://github.com/microsoft/typespec/pull/3401) Update dependencies - May 2024 + +### Features + +- [#3465](https://github.com/microsoft/typespec/pull/3465) Provide ability to add custom program viewers +- [#3569](https://github.com/microsoft/typespec/pull/3569) Support loglevel in playground's logging + + ## 0.2.2 ### Bug Fixes diff --git a/packages/playground/package.json b/packages/playground/package.json index 6476f84a374..889942f6317 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -1,6 +1,6 @@ { "name": "@typespec/playground", - "version": "0.2.2", + "version": "0.3.0", "author": "Microsoft Corporation", "description": "TypeSpec playground UI components.", "homepage": "https://typespec.io", @@ -55,7 +55,6 @@ "watch": "vite build --watch", "storybook": "sb dev", "build: storybook": "sb build", - "copy-css": "copyfiles -u 1 src/**/*.module.css dist/src", "preview": "npm run build && vite preview", "start": "vite", "test": "echo 'no tests'", @@ -107,11 +106,12 @@ "@typespec/bundler": "workspace:~", "@vitejs/plugin-react": "~4.2.1", "c8": "^9.1.0", - "copyfiles": "~2.4.1", "cross-env": "~7.0.3", + "es-module-shims": "~1.10.0", "rimraf": "~5.0.7", "typescript": "~5.4.5", "vite": "^5.2.11", + "vite-plugin-checker": "^0.6.4", "vite-plugin-dts": "^3.9.1" } } diff --git a/packages/playground/src/core.ts b/packages/playground/src/core.ts index d006675fcbd..9cb24a27a81 100644 --- a/packages/playground/src/core.ts +++ b/packages/playground/src/core.ts @@ -10,7 +10,7 @@ export interface LibraryImportOptions { } export async function importTypeSpecCompiler( - config: any + config: LibraryImportOptions ): Promise { return (await importLibrary("@typespec/compiler", config)) as any; } diff --git a/packages/playground/src/react/default-footer.tsx b/packages/playground/src/react/default-footer.tsx index c551ffc44d5..5599a33ea64 100644 --- a/packages/playground/src/react/default-footer.tsx +++ b/packages/playground/src/react/default-footer.tsx @@ -1,4 +1,4 @@ -import { FunctionComponent } from "react"; +import type { FunctionComponent } from "react"; import { FooterVersionItem } from "./footer/footer-version-item.js"; import { Footer } from "./footer/index.js"; diff --git a/packages/playground/src/react/diagnostic-list/diagnostic-list.tsx b/packages/playground/src/react/diagnostic-list/diagnostic-list.tsx index 3ad4301d762..5c6c7e4128d 100644 --- a/packages/playground/src/react/diagnostic-list/diagnostic-list.tsx +++ b/packages/playground/src/react/diagnostic-list/diagnostic-list.tsx @@ -5,7 +5,7 @@ import { type DiagnosticTarget, type NoTarget, } from "@typespec/compiler"; -import { FunctionComponent, memo, useCallback } from "react"; +import { memo, useCallback, type FunctionComponent } from "react"; import style from "./diagnostic-list.module.css"; export interface DiagnosticListProps { diff --git a/packages/playground/src/react/editor.tsx b/packages/playground/src/react/editor.tsx index bb21acba396..1cdce05796b 100644 --- a/packages/playground/src/react/editor.tsx +++ b/packages/playground/src/react/editor.tsx @@ -53,7 +53,7 @@ export const Editor: FunctionComponent = ({ model, options, actions return (
diff --git a/packages/playground/src/react/emitter-dropdown.tsx b/packages/playground/src/react/emitter-dropdown.tsx index 4ca6386caf1..9b6c6f4e35a 100644 --- a/packages/playground/src/react/emitter-dropdown.tsx +++ b/packages/playground/src/react/emitter-dropdown.tsx @@ -1,5 +1,5 @@ import { Select } from "@fluentui/react-components"; -import { FunctionComponent, useCallback } from "react"; +import { useCallback, type FunctionComponent } from "react"; export type EmitterDropdownProps = { emitters: string[]; diff --git a/packages/playground/src/react/file-output/file-output.tsx b/packages/playground/src/react/file-output/file-output.tsx index 92d342bde0e..0e4505057f5 100644 --- a/packages/playground/src/react/file-output/file-output.tsx +++ b/packages/playground/src/react/file-output/file-output.tsx @@ -1,25 +1,35 @@ import { Select, type SelectOnChangeData } from "@fluentui/react-components"; import { useCallback, useMemo, useState, type FunctionComponent } from "react"; import type { FileOutputViewer } from "../types.js"; +import { OutputEditor } from "../typespec-editor.js"; import style from "./file-output.module.css"; export interface FileOutputProps { - filename: string; - content: string; - viewers: FileOutputViewer[]; + readonly filename: string; + readonly content: string; + readonly viewers: Record; } /** * Display a file output using different viewers. */ export const FileOutput: FunctionComponent = ({ filename, content, viewers }) => { - if (viewers.length === 0) { + const resolvedViewers: Record = useMemo( + () => ({ + [RawFileViewer.key]: RawFileViewer, + ...viewers, + }), + [viewers] + ); + const keys = Object.keys(resolvedViewers); + + if (keys.length === 0) { return <>No viewers; - } else if (viewers.length === 1) { - return viewers[0].render({ filename, content }); + } else if (keys.length === 1) { + return resolvedViewers[keys[0]].render({ filename, content }); } - const [selected, setSelected] = useState(viewers[0].key); + const [selected, setSelected] = useState(keys[0]); const handleSelected = useCallback( (_: unknown, data: SelectOnChangeData) => { @@ -29,13 +39,13 @@ export const FileOutput: FunctionComponent = ({ filename, conte ); const selectedRender = useMemo(() => { - return viewers.find((x) => x.key === selected)?.render; - }, [selected, viewers]); + return resolvedViewers[selected].render; + }, [selected, resolvedViewers]); return (