diff --git a/.chronus/changes/python-fix-optional-properties-2025-11-3-16-16-35.md b/.chronus/changes/HEAD-2025-11-16-13-29-48.md
similarity index 51%
rename from .chronus/changes/python-fix-optional-properties-2025-11-3-16-16-35.md
rename to .chronus/changes/HEAD-2025-11-16-13-29-48.md
index ac948c2b08e..1d38a28a02a 100644
--- a/.chronus/changes/python-fix-optional-properties-2025-11-3-16-16-35.md
+++ b/.chronus/changes/HEAD-2025-11-16-13-29-48.md
@@ -4,4 +4,4 @@ packages:
- "@typespec/http-client-python"
---
-Fix for optional properties in flatten model to keep compatibility
\ No newline at end of file
+fix client default value for special headers
\ No newline at end of file
diff --git a/.chronus/changes/alloy-0.22-2025-11-12-13-21-52.md b/.chronus/changes/alloy-0.22-2025-11-12-13-21-52.md
new file mode 100644
index 00000000000..723a280c815
--- /dev/null
+++ b/.chronus/changes/alloy-0.22-2025-11-12-13-21-52.md
@@ -0,0 +1,10 @@
+---
+changeKind: dependencies
+packages:
+ - "@typespec/emitter-framework"
+ - "@typespec/http-client-js"
+ - "@typespec/http-client"
+ - "@typespec/tspd"
+---
+
+Update to alloy 0.22
\ No newline at end of file
diff --git a/.chronus/changes/array-encoding-comma-newline-2025-11-13-20-53-33.md b/.chronus/changes/array-encoding-comma-newline-2025-11-13-20-53-33.md
deleted file mode 100644
index 2f6c1a3c6c7..00000000000
--- a/.chronus/changes/array-encoding-comma-newline-2025-11-13-20-53-33.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-changeKind: feature
-packages:
- - "@typespec/compiler"
- - "@typespec/openapi3"
----
-
-Add `commaDelimited` and `newlineDelimited` values to `ArrayEncoding` enum for serializing arrays with comma and newline delimiters
diff --git a/.chronus/changes/ef-updates-2025-11-4-17-44-46.md b/.chronus/changes/ef-updates-2025-11-4-17-44-46.md
new file mode 100644
index 00000000000..f790d7ec3d8
--- /dev/null
+++ b/.chronus/changes/ef-updates-2025-11-4-17-44-46.md
@@ -0,0 +1,8 @@
+---
+changeKind: breaking
+packages:
+ - "@typespec/http-canonicalization"
+ - "@typespec/mutator-framework"
+---
+
+Many other bug fixes, removals, and additions as described in this pull request: https://github.com/microsoft/typespec/pull/9141
\ No newline at end of file
diff --git a/.chronus/changes/ef-updates-2025-11-4-17-45-6.md b/.chronus/changes/ef-updates-2025-11-4-17-45-6.md
new file mode 100644
index 00000000000..1a7262852a6
--- /dev/null
+++ b/.chronus/changes/ef-updates-2025-11-4-17-45-6.md
@@ -0,0 +1,7 @@
+---
+changeKind: feature
+packages:
+ - "@typespec/emitter-framework"
+---
+
+Add an SCCSet class which incrementally calculates strongly connected components from types added to it and updates reactive arrays with the topologically ordered types and components.
\ No newline at end of file
diff --git a/.chronus/changes/external-type-python-2025-9-20-14-28-41.md b/.chronus/changes/external-type-python-2025-9-20-14-28-41.md
deleted file mode 100644
index 2977eb159e2..00000000000
--- a/.chronus/changes/external-type-python-2025-9-20-14-28-41.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-changeKind: feature
-packages:
- - "@typespec/http-client-python"
----
-
-Support SDK users defined customized serialization/deserialization function for external models
\ No newline at end of file
diff --git a/.chronus/changes/fionabronwen-mutator-framework-2025-11-3-22-19-48.md b/.chronus/changes/fionabronwen-mutator-framework-2025-11-3-22-19-48.md
deleted file mode 100644
index 5b28cf330b5..00000000000
--- a/.chronus/changes/fionabronwen-mutator-framework-2025-11-3-22-19-48.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
-changeKind: feature
-packages:
- - "@typespec/mutator-framework"
----
-
-Add `EnumMutation` and `EnumMemberMutation` to the Mutator Framework
diff --git a/.chronus/changes/fix-encode-duration-tests-2025-11-25-04-18-59.md b/.chronus/changes/fix-encode-duration-tests-2025-11-25-04-18-59.md
deleted file mode 100644
index 16a7b59f680..00000000000
--- a/.chronus/changes/fix-encode-duration-tests-2025-11-25-04-18-59.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-changeKind: fix
-packages:
- - "@typespec/http-specs"
----
-
-Fix EncodeDuration tests with larger unit durations being too strict by making query parameter expectations match input types as numbers instead of strings
diff --git a/.chronus/changes/fix-external-docs-properties-2025-10-24-18-22-12.md b/.chronus/changes/fix-problem-pane-2025-11-10-14-5-0.md
similarity index 62%
rename from .chronus/changes/fix-external-docs-properties-2025-10-24-18-22-12.md
rename to .chronus/changes/fix-problem-pane-2025-11-10-14-5-0.md
index f1e5a51a68d..ed464204af5 100644
--- a/.chronus/changes/fix-external-docs-properties-2025-10-24-18-22-12.md
+++ b/.chronus/changes/fix-problem-pane-2025-11-10-14-5-0.md
@@ -2,7 +2,7 @@
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: fix
packages:
- - "@typespec/openapi3"
+ - "@typespec/playground"
---
-Respect `@externalDocs` on properties
+[a11y] Fix problem pane not usable via keyboard
diff --git a/.chronus/changes/specs-addDocTest-2025-10-4-13-59-34.md b/.chronus/changes/fix-python-constant-query-new-2025-9-28-11-8-13.md
similarity index 64%
rename from .chronus/changes/specs-addDocTest-2025-10-4-13-59-34.md
rename to .chronus/changes/fix-python-constant-query-new-2025-9-28-11-8-13.md
index 229d5793b93..8ef5e81c029 100644
--- a/.chronus/changes/specs-addDocTest-2025-10-4-13-59-34.md
+++ b/.chronus/changes/fix-python-constant-query-new-2025-9-28-11-8-13.md
@@ -4,4 +4,4 @@ packages:
- "@typespec/http-specs"
---
-add test for documentation generation
\ No newline at end of file
+Add case for constant query
\ No newline at end of file
diff --git a/.chronus/changes/fix-signature-help-test-slow-2025-10-14-2-25-10.md b/.chronus/changes/fix-signature-help-test-slow-2025-10-14-2-25-10.md
deleted file mode 100644
index 5a703a4f8df..00000000000
--- a/.chronus/changes/fix-signature-help-test-slow-2025-10-14-2-25-10.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
-changeKind: internal
-packages:
- - "@typespec/compiler"
----
-
-Fix signature help test super slow
diff --git a/.chronus/changes/fix-spector-source-url-2025-10-12-10-38-17.md b/.chronus/changes/fix-spector-source-url-2025-10-12-10-38-17.md
deleted file mode 100644
index df937acedcd..00000000000
--- a/.chronus/changes/fix-spector-source-url-2025-10-12-10-38-17.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-changeKind: fix
-packages:
- - "@typespec/http-specs"
- - "@typespec/spec-coverage-sdk"
- - "@typespec/spector"
----
-
-Add new `sourceUrl` handling for the go to source navigation
\ No newline at end of file
diff --git a/.chronus/changes/linter-async-callback-2025-10-6-12-33-19.md b/.chronus/changes/linter-async-callback-2025-10-6-12-33-19.md
deleted file mode 100644
index b4aa8b1f17a..00000000000
--- a/.chronus/changes/linter-async-callback-2025-10-6-12-33-19.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-changeKind: feature
-packages:
- - "@typespec/compiler"
----
-
-- Add 'exit' final event for linter rules
-- Support 'async' in linter definition and async function as callback for 'exit' event.
diff --git a/.chronus/changes/main-2025-10-19-17-48-27.md b/.chronus/changes/main-2025-10-19-17-48-27.md
deleted file mode 100644
index 42ce7ad3260..00000000000
--- a/.chronus/changes/main-2025-10-19-17-48-27.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-changeKind: feature
-packages:
- - "@typespec/json-schema"
----
-
-Add discriminator support and polymorphic models strategy option
-
-- Automatically injects discriminator property into base models with `@discriminator` decorator
-- Marks discriminator property as required in generated schemas
-- New `polymorphic-models-strategy` emitter option with three strategies:
- - `ignore`: Emit as regular object schema (default)
- - `oneOf`: Emit oneOf schema for closed discriminated unions
- - `anyOf`: Emit anyOf schema for open discriminated unions
-- Includes discriminator.mapping in oneOf/anyOf schemas for improved validation
diff --git a/.chronus/changes/mictaylor-json-schema-discriminator-circular-ref-fix-2025-10-21-6-44-36.md b/.chronus/changes/mictaylor-json-schema-discriminator-circular-ref-fix-2025-10-21-6-44-36.md
deleted file mode 100644
index 73255d9578e..00000000000
--- a/.chronus/changes/mictaylor-json-schema-discriminator-circular-ref-fix-2025-10-21-6-44-36.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-changeKind: internal
-packages:
- - "@typespec/json-schema"
----
-
-avoid circular references in discriminated unions with polymorphic-models-strategy
\ No newline at end of file
diff --git a/.chronus/changes/mictaylor-json-schema-open-discriminator-support-2025-11-2-20-20-32.md b/.chronus/changes/mictaylor-json-schema-open-discriminator-support-2025-11-2-20-20-32.md
deleted file mode 100644
index 9b482e190ad..00000000000
--- a/.chronus/changes/mictaylor-json-schema-open-discriminator-support-2025-11-2-20-20-32.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-changeKind: internal
-packages:
- - "@typespec/json-schema"
----
-
-add support for open discriminators
diff --git a/.chronus/changes/named-unions-response-doc-2025-10-10-16-34-57.md b/.chronus/changes/named-unions-response-doc-2025-10-10-16-34-57.md
deleted file mode 100644
index 95efc2352e1..00000000000
--- a/.chronus/changes/named-unions-response-doc-2025-10-10-16-34-57.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-changeKind: feature
-packages:
- - "@typespec/http"
----
-
-support documentation on union variants for response descriptions
diff --git a/.chronus/changes/named-unions-response-type-2025-10-10-16-34-2.md b/.chronus/changes/named-unions-response-type-2025-10-10-16-34-2.md
deleted file mode 100644
index 29570e50e91..00000000000
--- a/.chronus/changes/named-unions-response-type-2025-10-10-16-34-2.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-changeKind: fix
-packages:
- - "@typespec/http"
- - "@typespec/openapi3"
----
-
-Support nested unions in operation return types
diff --git a/.chronus/changes/python-backcompatTspPadding-2025-10-18-17-23-22.md b/.chronus/changes/python-backcompatTspPadding-2025-10-18-17-23-22.md
deleted file mode 100644
index 3d266c69f9f..00000000000
--- a/.chronus/changes/python-backcompatTspPadding-2025-10-18-17-23-22.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-changeKind: fix
-packages:
- - "@typespec/http-client-python"
----
-
-Keep original client name for backcompat reasons when the name is only padded for tsp generations
\ No newline at end of file
diff --git a/.chronus/changes/python-flatten-model-initialization-test-2025-11-1-8-33-43.md b/.chronus/changes/python-flatten-model-initialization-test-2025-11-1-8-33-43.md
deleted file mode 100644
index 19855d95798..00000000000
--- a/.chronus/changes/python-flatten-model-initialization-test-2025-11-1-8-33-43.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-changeKind: internal
-packages:
- - "@typespec/http-client-python"
----
-
-Add test case for flatten model initialization to check compatibility
\ No newline at end of file
diff --git a/.chronus/changes/remove-js-yaml-2025-10-17-18-13-50.md b/.chronus/changes/remove-js-yaml-2025-10-17-18-13-50.md
deleted file mode 100644
index 2fc2e9ba41b..00000000000
--- a/.chronus/changes/remove-js-yaml-2025-10-17-18-13-50.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
-changeKind: fix
-packages:
- - "@typespec/spector"
----
-
-Switch `js-yaml` to `yaml` library
diff --git a/.chronus/changes/source-model-node-2025-10-18-15-0-36.md b/.chronus/changes/source-model-node-2025-10-18-15-0-36.md
deleted file mode 100644
index 35cc9a9177c..00000000000
--- a/.chronus/changes/source-model-node-2025-10-18-15-0-36.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-changeKind: feature
-packages:
- - "@typespec/compiler"
----
-
-[API] Add `node` to `SourceModel` type
\ No newline at end of file
diff --git a/.chronus/changes/spector-new-case-2025-10-18-7-0-48.md b/.chronus/changes/spector-new-case-2025-10-18-7-0-48.md
deleted file mode 100644
index 50498f86a84..00000000000
--- a/.chronus/changes/spector-new-case-2025-10-18-7-0-48.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-changeKind: feature
-packages:
- - "@typespec/http-specs"
----
-
-Add test case for special words about model property name
\ No newline at end of file
diff --git a/.chronus/changes/syntax-highlighting-not-correctly-recognize-hyphen-param-2025-8-30-6-59-7.md b/.chronus/changes/syntax-highlighting-not-correctly-recognize-hyphen-param-2025-8-30-6-59-7.md
deleted file mode 100644
index 6bb5e75d6ec..00000000000
--- a/.chronus/changes/syntax-highlighting-not-correctly-recognize-hyphen-param-2025-8-30-6-59-7.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
-changeKind: feature
-packages:
- - "@typespec/compiler"
----
-
-Add support for escaping param like tags(`@param`, `@prop`, etc.) identifier with backtick in doc comments to allow special characters
diff --git a/.chronus/changes/unused-as-hint-2025-10-13-17-12-1.md b/.chronus/changes/unused-as-hint-2025-10-13-17-12-1.md
deleted file mode 100644
index f53472dfb3d..00000000000
--- a/.chronus/changes/unused-as-hint-2025-10-13-17-12-1.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-changeKind: fix
-packages:
- - "@typespec/compiler"
----
-
-UnusedUsing Diagnostics are reported as warning instead of hint when there are linters defined in tspconfig.yaml
diff --git a/.chronus/changes/updates-2025-10-11-11-38-50.md b/.chronus/changes/updates-2025-10-11-11-38-50.md
new file mode 100644
index 00000000000..943280b4433
--- /dev/null
+++ b/.chronus/changes/updates-2025-10-11-11-38-50.md
@@ -0,0 +1,7 @@
+---
+changeKind: breaking
+packages:
+ - "@typespec/mutator-framework"
+---
+
+Fix mutations not handling linkage to parent types (e.g. model property -> model). Remove mutation subgraph, nodes are now unique per (type, key) pair. Remove reference mutations, use a distinct key for references if needed.
\ No newline at end of file
diff --git a/.chronus/changes/updates-2025-10-11-11-39-17.md b/.chronus/changes/updates-2025-10-11-11-39-17.md
new file mode 100644
index 00000000000..6846af685f1
--- /dev/null
+++ b/.chronus/changes/updates-2025-10-11-11-39-17.md
@@ -0,0 +1,7 @@
+---
+changeKind: fix
+packages:
+ - "@typespec/http-canonicalization"
+---
+
+Fix canonicalization of merge patch models.
\ No newline at end of file
diff --git a/.chronus/changes/updates-2025-10-11-11-39-37.md b/.chronus/changes/updates-2025-10-11-11-39-37.md
new file mode 100644
index 00000000000..f1578a2537b
--- /dev/null
+++ b/.chronus/changes/updates-2025-10-11-11-39-37.md
@@ -0,0 +1,7 @@
+---
+changeKind: fix
+packages:
+ - "@typespec/http-canonicalization"
+---
+
+Remove metadata properties from wire types.
\ No newline at end of file
diff --git a/.chronus/changes/upgrade-deps-nov-2025-2025-10-20-16-42-0.md b/.chronus/changes/upgrade-deps-nov-2025-2025-10-20-16-42-0.md
deleted file mode 100644
index 7cf26ce1ae4..00000000000
--- a/.chronus/changes/upgrade-deps-nov-2025-2025-10-20-16-42-0.md
+++ /dev/null
@@ -1,40 +0,0 @@
----
-# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
-changeKind: dependencies
-packages:
- - "@typespec/asset-emitter"
- - "@typespec/bundler"
- - "@typespec/compiler"
- - "@typespec/eslint-plugin"
- - "@typespec/events"
- - "@typespec/html-program-viewer"
- - "@typespec/http-specs"
- - "@typespec/http"
- - "@typespec/internal-build-utils"
- - "@typespec/json-schema"
- - "@typespec/library-linter"
- - "@typespec/openapi"
- - "@typespec/openapi3"
- - "@typespec/playground"
- - "@typespec/protobuf"
- - "@typespec/rest"
- - "@typespec/spec-api"
- - "@typespec/spec-coverage-sdk"
- - "@typespec/spector"
- - "@typespec/sse"
- - "@typespec/streams"
- - tmlanguage-generator
- - "@typespec/tspd"
- - typespec-vscode
- - "@typespec/versioning"
- - "@typespec/xml"
- - "@typespec/http-canonicalization"
- - "@typespec/http-client-js"
- - "@typespec/http-client"
- - "@typespec/http-server-csharp"
- - "@typespec/http-server-js"
- - "@typespec/mutator-framework"
- - "@typespec/prettier-plugin-typespec"
----
-
-Upgrade dependencies
diff --git a/.chronus/changes/witemple-msft-hsjs-hyperparam-optionality-2025-11-10-11-43-24.md b/.chronus/changes/witemple-msft-hsjs-hyperparam-optionality-2025-11-10-11-43-24.md
new file mode 100644
index 00000000000..0fab7340042
--- /dev/null
+++ b/.chronus/changes/witemple-msft-hsjs-hyperparam-optionality-2025-11-10-11-43-24.md
@@ -0,0 +1,7 @@
+---
+changeKind: fix
+packages:
+ - "@typespec/http-server-js"
+---
+
+Fixed a bug that caused optional query/header parameters to be improperly converted to primitive types when not provided in a request.
\ No newline at end of file
diff --git a/cspell.yaml b/cspell.yaml
index 3d5a5009cdb..98e7cc43a05 100644
--- a/cspell.yaml
+++ b/cspell.yaml
@@ -142,8 +142,6 @@ words:
- lropaging
- lstrip
- lzutf
- - MACVMIMAGE
- - MACVMIMAGEM
- mday
- methodsubscriptionid
- mgmt
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/AnimalOperations.RestClient.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/AnimalOperations.RestClient.cs
new file mode 100644
index 00000000000..5798e0deb91
--- /dev/null
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/AnimalOperations.RestClient.cs
@@ -0,0 +1,45 @@
+//
+
+#nullable disable
+
+using System.ClientModel;
+using System.ClientModel.Primitives;
+
+namespace SampleTypeSpec
+{
+ ///
+ public partial class AnimalOperations
+ {
+ private static PipelineMessageClassifier _pipelineMessageClassifier200;
+
+ private static PipelineMessageClassifier PipelineMessageClassifier200 => _pipelineMessageClassifier200 = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 });
+
+ internal PipelineMessage CreateUpdatePetAsAnimalRequest(BinaryContent content, RequestOptions options)
+ {
+ ClientUriBuilder uri = new ClientUriBuilder();
+ uri.Reset(_endpoint);
+ uri.AppendPath("/animals/pet/as-animal", false);
+ PipelineMessage message = Pipeline.CreateMessage(uri.ToUri(), "PUT", PipelineMessageClassifier200);
+ PipelineRequest request = message.Request;
+ request.Headers.Set("Content-Type", "application/json");
+ request.Headers.Set("Accept", "application/json");
+ request.Content = content;
+ message.Apply(options);
+ return message;
+ }
+
+ internal PipelineMessage CreateUpdateDogAsAnimalRequest(BinaryContent content, RequestOptions options)
+ {
+ ClientUriBuilder uri = new ClientUriBuilder();
+ uri.Reset(_endpoint);
+ uri.AppendPath("/animals/dog/as-animal", false);
+ PipelineMessage message = Pipeline.CreateMessage(uri.ToUri(), "PUT", PipelineMessageClassifier200);
+ PipelineRequest request = message.Request;
+ request.Headers.Set("Content-Type", "application/json");
+ request.Headers.Set("Accept", "application/json");
+ request.Content = content;
+ message.Apply(options);
+ return message;
+ }
+ }
+}
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/AnimalOperations.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/AnimalOperations.cs
new file mode 100644
index 00000000000..39ba0f88e01
--- /dev/null
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/AnimalOperations.cs
@@ -0,0 +1,275 @@
+//
+
+#nullable disable
+
+using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SampleTypeSpec
+{
+ /// The AnimalOperations sub-client.
+ public partial class AnimalOperations
+ {
+ private readonly Uri _endpoint;
+
+ /// Initializes a new instance of AnimalOperations for mocking.
+ protected AnimalOperations()
+ {
+ }
+
+ /// Initializes a new instance of AnimalOperations.
+ /// The HTTP pipeline for sending and receiving REST requests and responses.
+ /// Service endpoint.
+ internal AnimalOperations(ClientPipeline pipeline, Uri endpoint)
+ {
+ _endpoint = endpoint;
+ Pipeline = pipeline;
+ }
+
+ /// The HTTP pipeline for sending and receiving REST requests and responses.
+ public ClientPipeline Pipeline { get; }
+
+ ///
+ /// [Protocol Method] Update a pet as an animal
+ ///
+ /// -
+ /// This protocol method allows explicit creation of the request and processing of the response for advanced scenarios.
+ ///
+ ///
+ ///
+ /// The content to send as the body of the request.
+ /// The request options, which can override default behaviors of the client pipeline on a per-call basis.
+ /// is null.
+ /// Service returned a non-success status code.
+ /// The response returned from the service.
+ public virtual ClientResult UpdatePetAsAnimal(BinaryContent content, RequestOptions options = null)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdatePetAsAnimal.");
+ Argument.AssertNotNull(content, nameof(content));
+
+ using PipelineMessage message = CreateUpdatePetAsAnimalRequest(content, options);
+ return ClientResult.FromResponse(Pipeline.ProcessMessage(message, options));
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdatePetAsAnimal: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdatePetAsAnimal.");
+ }
+ }
+
+ ///
+ /// [Protocol Method] Update a pet as an animal
+ ///
+ /// -
+ /// This protocol method allows explicit creation of the request and processing of the response for advanced scenarios.
+ ///
+ ///
+ ///
+ /// The content to send as the body of the request.
+ /// The request options, which can override default behaviors of the client pipeline on a per-call basis.
+ /// is null.
+ /// Service returned a non-success status code.
+ /// The response returned from the service.
+ public virtual async Task UpdatePetAsAnimalAsync(BinaryContent content, RequestOptions options = null)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdatePetAsAnimalAsync.");
+ Argument.AssertNotNull(content, nameof(content));
+
+ using PipelineMessage message = CreateUpdatePetAsAnimalRequest(content, options);
+ return ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false));
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdatePetAsAnimalAsync: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdatePetAsAnimalAsync.");
+ }
+ }
+
+ /// Update a pet as an animal.
+ ///
+ /// The cancellation token that can be used to cancel the operation.
+ /// is null.
+ /// Service returned a non-success status code.
+ public virtual ClientResult UpdatePetAsAnimal(Animal animal, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdatePetAsAnimal.");
+ Argument.AssertNotNull(animal, nameof(animal));
+
+ ClientResult result = UpdatePetAsAnimal(animal, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null);
+ return ClientResult.FromValue((Animal)result, result.GetRawResponse());
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdatePetAsAnimal: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdatePetAsAnimal.");
+ }
+ }
+
+ /// Update a pet as an animal.
+ ///
+ /// The cancellation token that can be used to cancel the operation.
+ /// is null.
+ /// Service returned a non-success status code.
+ public virtual async Task> UpdatePetAsAnimalAsync(Animal animal, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdatePetAsAnimalAsync.");
+ Argument.AssertNotNull(animal, nameof(animal));
+
+ ClientResult result = await UpdatePetAsAnimalAsync(animal, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false);
+ return ClientResult.FromValue((Animal)result, result.GetRawResponse());
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdatePetAsAnimalAsync: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdatePetAsAnimalAsync.");
+ }
+ }
+
+ ///
+ /// [Protocol Method] Update a dog as an animal
+ ///
+ /// -
+ /// This protocol method allows explicit creation of the request and processing of the response for advanced scenarios.
+ ///
+ ///
+ ///
+ /// The content to send as the body of the request.
+ /// The request options, which can override default behaviors of the client pipeline on a per-call basis.
+ /// is null.
+ /// Service returned a non-success status code.
+ /// The response returned from the service.
+ public virtual ClientResult UpdateDogAsAnimal(BinaryContent content, RequestOptions options = null)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdateDogAsAnimal.");
+ Argument.AssertNotNull(content, nameof(content));
+
+ using PipelineMessage message = CreateUpdateDogAsAnimalRequest(content, options);
+ return ClientResult.FromResponse(Pipeline.ProcessMessage(message, options));
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdateDogAsAnimal: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdateDogAsAnimal.");
+ }
+ }
+
+ ///
+ /// [Protocol Method] Update a dog as an animal
+ ///
+ /// -
+ /// This protocol method allows explicit creation of the request and processing of the response for advanced scenarios.
+ ///
+ ///
+ ///
+ /// The content to send as the body of the request.
+ /// The request options, which can override default behaviors of the client pipeline on a per-call basis.
+ /// is null.
+ /// Service returned a non-success status code.
+ /// The response returned from the service.
+ public virtual async Task UpdateDogAsAnimalAsync(BinaryContent content, RequestOptions options = null)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdateDogAsAnimalAsync.");
+ Argument.AssertNotNull(content, nameof(content));
+
+ using PipelineMessage message = CreateUpdateDogAsAnimalRequest(content, options);
+ return ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false));
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdateDogAsAnimalAsync: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdateDogAsAnimalAsync.");
+ }
+ }
+
+ /// Update a dog as an animal.
+ ///
+ /// The cancellation token that can be used to cancel the operation.
+ /// is null.
+ /// Service returned a non-success status code.
+ public virtual ClientResult UpdateDogAsAnimal(Animal animal, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdateDogAsAnimal.");
+ Argument.AssertNotNull(animal, nameof(animal));
+
+ ClientResult result = UpdateDogAsAnimal(animal, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null);
+ return ClientResult.FromValue((Animal)result, result.GetRawResponse());
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdateDogAsAnimal: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdateDogAsAnimal.");
+ }
+ }
+
+ /// Update a dog as an animal.
+ ///
+ /// The cancellation token that can be used to cancel the operation.
+ /// is null.
+ /// Service returned a non-success status code.
+ public virtual async Task> UpdateDogAsAnimalAsync(Animal animal, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdateDogAsAnimalAsync.");
+ Argument.AssertNotNull(animal, nameof(animal));
+
+ ClientResult result = await UpdateDogAsAnimalAsync(animal, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false);
+ return ClientResult.FromValue((Animal)result, result.GetRawResponse());
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdateDogAsAnimalAsync: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdateDogAsAnimalAsync.");
+ }
+ }
+ }
+}
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/DogOperations.RestClient.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/DogOperations.RestClient.cs
new file mode 100644
index 00000000000..3b628c186da
--- /dev/null
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/DogOperations.RestClient.cs
@@ -0,0 +1,31 @@
+//
+
+#nullable disable
+
+using System.ClientModel;
+using System.ClientModel.Primitives;
+
+namespace SampleTypeSpec
+{
+ ///
+ public partial class DogOperations
+ {
+ private static PipelineMessageClassifier _pipelineMessageClassifier200;
+
+ private static PipelineMessageClassifier PipelineMessageClassifier200 => _pipelineMessageClassifier200 = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 });
+
+ internal PipelineMessage CreateUpdateDogAsDogRequest(BinaryContent content, RequestOptions options)
+ {
+ ClientUriBuilder uri = new ClientUriBuilder();
+ uri.Reset(_endpoint);
+ uri.AppendPath("/dogs/dog/as-dog", false);
+ PipelineMessage message = Pipeline.CreateMessage(uri.ToUri(), "PUT", PipelineMessageClassifier200);
+ PipelineRequest request = message.Request;
+ request.Headers.Set("Content-Type", "application/json");
+ request.Headers.Set("Accept", "application/json");
+ request.Content = content;
+ message.Apply(options);
+ return message;
+ }
+ }
+}
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/DogOperations.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/DogOperations.cs
new file mode 100644
index 00000000000..553adb576a1
--- /dev/null
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/DogOperations.cs
@@ -0,0 +1,155 @@
+//
+
+#nullable disable
+
+using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SampleTypeSpec
+{
+ /// The DogOperations sub-client.
+ public partial class DogOperations
+ {
+ private readonly Uri _endpoint;
+
+ /// Initializes a new instance of DogOperations for mocking.
+ protected DogOperations()
+ {
+ }
+
+ /// Initializes a new instance of DogOperations.
+ /// The HTTP pipeline for sending and receiving REST requests and responses.
+ /// Service endpoint.
+ internal DogOperations(ClientPipeline pipeline, Uri endpoint)
+ {
+ _endpoint = endpoint;
+ Pipeline = pipeline;
+ }
+
+ /// The HTTP pipeline for sending and receiving REST requests and responses.
+ public ClientPipeline Pipeline { get; }
+
+ ///
+ /// [Protocol Method] Update a dog as a dog
+ ///
+ /// -
+ /// This protocol method allows explicit creation of the request and processing of the response for advanced scenarios.
+ ///
+ ///
+ ///
+ /// The content to send as the body of the request.
+ /// The request options, which can override default behaviors of the client pipeline on a per-call basis.
+ /// is null.
+ /// Service returned a non-success status code.
+ /// The response returned from the service.
+ public virtual ClientResult UpdateDogAsDog(BinaryContent content, RequestOptions options = null)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdateDogAsDog.");
+ Argument.AssertNotNull(content, nameof(content));
+
+ using PipelineMessage message = CreateUpdateDogAsDogRequest(content, options);
+ return ClientResult.FromResponse(Pipeline.ProcessMessage(message, options));
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdateDogAsDog: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdateDogAsDog.");
+ }
+ }
+
+ ///
+ /// [Protocol Method] Update a dog as a dog
+ ///
+ /// -
+ /// This protocol method allows explicit creation of the request and processing of the response for advanced scenarios.
+ ///
+ ///
+ ///
+ /// The content to send as the body of the request.
+ /// The request options, which can override default behaviors of the client pipeline on a per-call basis.
+ /// is null.
+ /// Service returned a non-success status code.
+ /// The response returned from the service.
+ public virtual async Task UpdateDogAsDogAsync(BinaryContent content, RequestOptions options = null)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdateDogAsDogAsync.");
+ Argument.AssertNotNull(content, nameof(content));
+
+ using PipelineMessage message = CreateUpdateDogAsDogRequest(content, options);
+ return ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false));
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdateDogAsDogAsync: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdateDogAsDogAsync.");
+ }
+ }
+
+ /// Update a dog as a dog.
+ ///
+ /// The cancellation token that can be used to cancel the operation.
+ /// is null.
+ /// Service returned a non-success status code.
+ public virtual ClientResult UpdateDogAsDog(Dog dog, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdateDogAsDog.");
+ Argument.AssertNotNull(dog, nameof(dog));
+
+ ClientResult result = UpdateDogAsDog(dog, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null);
+ return ClientResult.FromValue((Dog)result, result.GetRawResponse());
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdateDogAsDog: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdateDogAsDog.");
+ }
+ }
+
+ /// Update a dog as a dog.
+ ///
+ /// The cancellation token that can be used to cancel the operation.
+ /// is null.
+ /// Service returned a non-success status code.
+ public virtual async Task> UpdateDogAsDogAsync(Dog dog, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdateDogAsDogAsync.");
+ Argument.AssertNotNull(dog, nameof(dog));
+
+ ClientResult result = await UpdateDogAsDogAsync(dog, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false);
+ return ClientResult.FromValue((Dog)result, result.GetRawResponse());
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdateDogAsDogAsync: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdateDogAsDogAsync.");
+ }
+ }
+ }
+}
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/Animal.Serialization.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/Animal.Serialization.cs
new file mode 100644
index 00000000000..4b8a05c0640
--- /dev/null
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/Animal.Serialization.cs
@@ -0,0 +1,159 @@
+//
+
+#nullable disable
+
+using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using System.Text.Json;
+
+namespace SampleTypeSpec
+{
+ ///
+ /// Base animal with discriminator
+ /// Please note this is the abstract base class. The derived classes available for instantiation are: and .
+ ///
+ [PersistableModelProxy(typeof(UnknownAnimal))]
+ public abstract partial class Animal : IJsonModel
+ {
+ /// Initializes a new instance of for deserialization.
+ internal Animal()
+ {
+ }
+
+ /// The JSON writer.
+ /// The client options for reading and writing models.
+ void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options)
+ {
+ writer.WriteStartObject();
+ JsonModelWriteCore(writer, options);
+ writer.WriteEndObject();
+ }
+
+ /// The JSON writer.
+ /// The client options for reading and writing models.
+ protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ if (format != "J")
+ {
+ throw new FormatException($"The model {nameof(Animal)} does not support writing '{format}' format.");
+ }
+ writer.WritePropertyName("kind"u8);
+ writer.WriteStringValue(Kind);
+ writer.WritePropertyName("name"u8);
+ writer.WriteStringValue(Name);
+ if (options.Format != "W" && _additionalBinaryDataProperties != null)
+ {
+ foreach (var item in _additionalBinaryDataProperties)
+ {
+ writer.WritePropertyName(item.Key);
+#if NET6_0_OR_GREATER
+ writer.WriteRawValue(item.Value);
+#else
+ using (JsonDocument document = JsonDocument.Parse(item.Value))
+ {
+ JsonSerializer.Serialize(writer, document.RootElement);
+ }
+#endif
+ }
+ }
+ }
+
+ /// The JSON reader.
+ /// The client options for reading and writing models.
+ Animal IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) => JsonModelCreateCore(ref reader, options);
+
+ /// The JSON reader.
+ /// The client options for reading and writing models.
+ protected virtual Animal JsonModelCreateCore(ref Utf8JsonReader reader, ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ if (format != "J")
+ {
+ throw new FormatException($"The model {nameof(Animal)} does not support reading '{format}' format.");
+ }
+ using JsonDocument document = JsonDocument.ParseValue(ref reader);
+ return DeserializeAnimal(document.RootElement, options);
+ }
+
+ /// The JSON element to deserialize.
+ /// The client options for reading and writing models.
+ internal static Animal DeserializeAnimal(JsonElement element, ModelReaderWriterOptions options)
+ {
+ if (element.ValueKind == JsonValueKind.Null)
+ {
+ return null;
+ }
+ if (element.TryGetProperty("kind"u8, out JsonElement discriminator))
+ {
+ switch (discriminator.GetString())
+ {
+ case "pet":
+ return Pet.DeserializePet(element, options);
+ case "dog":
+ return Dog.DeserializeDog(element, options);
+ }
+ }
+ return UnknownAnimal.DeserializeUnknownAnimal(element, options);
+ }
+
+ /// The client options for reading and writing models.
+ BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options);
+
+ /// The client options for reading and writing models.
+ protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ switch (format)
+ {
+ case "J":
+ return ModelReaderWriter.Write(this, options, SampleTypeSpecContext.Default);
+ default:
+ throw new FormatException($"The model {nameof(Animal)} does not support writing '{options.Format}' format.");
+ }
+ }
+
+ /// The data to parse.
+ /// The client options for reading and writing models.
+ Animal IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => PersistableModelCreateCore(data, options);
+
+ /// The data to parse.
+ /// The client options for reading and writing models.
+ protected virtual Animal PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ switch (format)
+ {
+ case "J":
+ using (JsonDocument document = JsonDocument.Parse(data))
+ {
+ return DeserializeAnimal(document.RootElement, options);
+ }
+ default:
+ throw new FormatException($"The model {nameof(Animal)} does not support reading '{options.Format}' format.");
+ }
+ }
+
+ /// The client options for reading and writing models.
+ string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "J";
+
+ /// The to serialize into .
+ public static implicit operator BinaryContent(Animal animal)
+ {
+ if (animal == null)
+ {
+ return null;
+ }
+ return BinaryContent.Create(animal, ModelSerializationExtensions.WireOptions);
+ }
+
+ /// The to deserialize the from.
+ public static explicit operator Animal(ClientResult result)
+ {
+ using PipelineResponse response = result.GetRawResponse();
+ using JsonDocument document = JsonDocument.Parse(response.Content);
+ return DeserializeAnimal(document.RootElement, ModelSerializationExtensions.WireOptions);
+ }
+ }
+}
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/Animal.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/Animal.cs
new file mode 100644
index 00000000000..95a223d3cac
--- /dev/null
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/Animal.cs
@@ -0,0 +1,45 @@
+//
+
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+
+namespace SampleTypeSpec
+{
+ ///
+ /// Base animal with discriminator
+ /// Please note this is the abstract base class. The derived classes available for instantiation are: and .
+ ///
+ public abstract partial class Animal
+ {
+ /// Keeps track of any properties unknown to the library.
+ private protected readonly IDictionary _additionalBinaryDataProperties;
+
+ /// Initializes a new instance of .
+ /// The kind of animal.
+ /// Name of the animal.
+ private protected Animal(string kind, string name)
+ {
+ Kind = kind;
+ Name = name;
+ }
+
+ /// Initializes a new instance of .
+ /// The kind of animal.
+ /// Name of the animal.
+ /// Keeps track of any properties unknown to the library.
+ internal Animal(string kind, string name, IDictionary additionalBinaryDataProperties)
+ {
+ Kind = kind;
+ Name = name;
+ _additionalBinaryDataProperties = additionalBinaryDataProperties;
+ }
+
+ /// The kind of animal.
+ internal string Kind { get; set; }
+
+ /// Name of the animal.
+ public string Name { get; set; }
+ }
+}
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/Dog.Serialization.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/Dog.Serialization.cs
new file mode 100644
index 00000000000..7d2e62bb48a
--- /dev/null
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/Dog.Serialization.cs
@@ -0,0 +1,162 @@
+//
+
+#nullable disable
+
+using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+using System.Text.Json;
+
+namespace SampleTypeSpec
+{
+ /// Dog is a specific type of pet with hierarchy building.
+ public partial class Dog : Pet, IJsonModel
+ {
+ /// Initializes a new instance of for deserialization.
+ internal Dog()
+ {
+ }
+
+ /// The JSON writer.
+ /// The client options for reading and writing models.
+ void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options)
+ {
+ writer.WriteStartObject();
+ JsonModelWriteCore(writer, options);
+ writer.WriteEndObject();
+ }
+
+ /// The JSON writer.
+ /// The client options for reading and writing models.
+ protected override void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ if (format != "J")
+ {
+ throw new FormatException($"The model {nameof(Dog)} does not support writing '{format}' format.");
+ }
+ base.JsonModelWriteCore(writer, options);
+ writer.WritePropertyName("breed"u8);
+ writer.WriteStringValue(Breed);
+ }
+
+ /// The JSON reader.
+ /// The client options for reading and writing models.
+ Dog IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) => (Dog)JsonModelCreateCore(ref reader, options);
+
+ /// The JSON reader.
+ /// The client options for reading and writing models.
+ protected override Animal JsonModelCreateCore(ref Utf8JsonReader reader, ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ if (format != "J")
+ {
+ throw new FormatException($"The model {nameof(Dog)} does not support reading '{format}' format.");
+ }
+ using JsonDocument document = JsonDocument.ParseValue(ref reader);
+ return DeserializeDog(document.RootElement, options);
+ }
+
+ /// The JSON element to deserialize.
+ /// The client options for reading and writing models.
+ internal static Dog DeserializeDog(JsonElement element, ModelReaderWriterOptions options)
+ {
+ if (element.ValueKind == JsonValueKind.Null)
+ {
+ return null;
+ }
+ string kind = "dog";
+ string name = default;
+ IDictionary additionalBinaryDataProperties = new ChangeTrackingDictionary();
+ bool trained = default;
+ string breed = default;
+ foreach (var prop in element.EnumerateObject())
+ {
+ if (prop.NameEquals("kind"u8))
+ {
+ kind = prop.Value.GetString();
+ continue;
+ }
+ if (prop.NameEquals("name"u8))
+ {
+ name = prop.Value.GetString();
+ continue;
+ }
+ if (prop.NameEquals("trained"u8))
+ {
+ trained = prop.Value.GetBoolean();
+ continue;
+ }
+ if (prop.NameEquals("breed"u8))
+ {
+ breed = prop.Value.GetString();
+ continue;
+ }
+ if (options.Format != "W")
+ {
+ additionalBinaryDataProperties.Add(prop.Name, BinaryData.FromString(prop.Value.GetRawText()));
+ }
+ }
+ return new Dog(kind, name, additionalBinaryDataProperties, trained, breed);
+ }
+
+ /// The client options for reading and writing models.
+ BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options);
+
+ /// The client options for reading and writing models.
+ protected override BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ switch (format)
+ {
+ case "J":
+ return ModelReaderWriter.Write(this, options, SampleTypeSpecContext.Default);
+ default:
+ throw new FormatException($"The model {nameof(Dog)} does not support writing '{options.Format}' format.");
+ }
+ }
+
+ /// The data to parse.
+ /// The client options for reading and writing models.
+ Dog IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => (Dog)PersistableModelCreateCore(data, options);
+
+ /// The data to parse.
+ /// The client options for reading and writing models.
+ protected override Animal PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ switch (format)
+ {
+ case "J":
+ using (JsonDocument document = JsonDocument.Parse(data))
+ {
+ return DeserializeDog(document.RootElement, options);
+ }
+ default:
+ throw new FormatException($"The model {nameof(Dog)} does not support reading '{options.Format}' format.");
+ }
+ }
+
+ /// The client options for reading and writing models.
+ string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "J";
+
+ /// The to serialize into .
+ public static implicit operator BinaryContent(Dog dog)
+ {
+ if (dog == null)
+ {
+ return null;
+ }
+ return BinaryContent.Create(dog, ModelSerializationExtensions.WireOptions);
+ }
+
+ /// The to deserialize the from.
+ public static explicit operator Dog(ClientResult result)
+ {
+ using PipelineResponse response = result.GetRawResponse();
+ using JsonDocument document = JsonDocument.Parse(response.Content);
+ return DeserializeDog(document.RootElement, ModelSerializationExtensions.WireOptions);
+ }
+ }
+}
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/Dog.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/Dog.cs
new file mode 100644
index 00000000000..29fc705738f
--- /dev/null
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/Dog.cs
@@ -0,0 +1,40 @@
+//
+
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+
+namespace SampleTypeSpec
+{
+ /// Dog is a specific type of pet with hierarchy building.
+ public partial class Dog : Pet
+ {
+ /// Initializes a new instance of .
+ /// Name of the animal.
+ /// Whether the pet is trained.
+ /// The breed of the dog.
+ /// or is null.
+ public Dog(string name, bool trained, string breed) : base(name, trained)
+ {
+ Argument.AssertNotNull(name, nameof(name));
+ Argument.AssertNotNull(breed, nameof(breed));
+
+ Breed = breed;
+ }
+
+ /// Initializes a new instance of .
+ /// The kind of animal.
+ /// Name of the animal.
+ /// Keeps track of any properties unknown to the library.
+ /// Whether the pet is trained.
+ /// The breed of the dog.
+ internal Dog(string kind, string name, IDictionary additionalBinaryDataProperties, bool trained, string breed) : base(kind, name, additionalBinaryDataProperties, trained)
+ {
+ Breed = breed;
+ }
+
+ /// The breed of the dog.
+ public string Breed { get; set; }
+ }
+}
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/Pet.Serialization.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/Pet.Serialization.cs
new file mode 100644
index 00000000000..1102daad746
--- /dev/null
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/Pet.Serialization.cs
@@ -0,0 +1,129 @@
+//
+
+#nullable disable
+
+using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using System.Text.Json;
+
+namespace SampleTypeSpec
+{
+ /// Pet is a discriminated animal.
+ public partial class Pet : Animal, IJsonModel
+ {
+ /// Initializes a new instance of for deserialization.
+ internal Pet()
+ {
+ }
+
+ /// The JSON writer.
+ /// The client options for reading and writing models.
+ void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options)
+ {
+ writer.WriteStartObject();
+ JsonModelWriteCore(writer, options);
+ writer.WriteEndObject();
+ }
+
+ /// The JSON writer.
+ /// The client options for reading and writing models.
+ protected override void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ if (format != "J")
+ {
+ throw new FormatException($"The model {nameof(Pet)} does not support writing '{format}' format.");
+ }
+ base.JsonModelWriteCore(writer, options);
+ writer.WritePropertyName("trained"u8);
+ writer.WriteBooleanValue(Trained);
+ }
+
+ /// The JSON reader.
+ /// The client options for reading and writing models.
+ Pet IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) => (Pet)JsonModelCreateCore(ref reader, options);
+
+ /// The JSON reader.
+ /// The client options for reading and writing models.
+ protected override Animal JsonModelCreateCore(ref Utf8JsonReader reader, ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ if (format != "J")
+ {
+ throw new FormatException($"The model {nameof(Pet)} does not support reading '{format}' format.");
+ }
+ using JsonDocument document = JsonDocument.ParseValue(ref reader);
+ return DeserializePet(document.RootElement, options);
+ }
+
+ /// The JSON element to deserialize.
+ /// The client options for reading and writing models.
+ internal static Pet DeserializePet(JsonElement element, ModelReaderWriterOptions options)
+ {
+ if (element.ValueKind == JsonValueKind.Null)
+ {
+ return null;
+ }
+ return UnknownPet.DeserializeUnknownPet(element, options);
+ }
+
+ /// The client options for reading and writing models.
+ BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options);
+
+ /// The client options for reading and writing models.
+ protected override BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ switch (format)
+ {
+ case "J":
+ return ModelReaderWriter.Write(this, options, SampleTypeSpecContext.Default);
+ default:
+ throw new FormatException($"The model {nameof(Pet)} does not support writing '{options.Format}' format.");
+ }
+ }
+
+ /// The data to parse.
+ /// The client options for reading and writing models.
+ Pet IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => (Pet)PersistableModelCreateCore(data, options);
+
+ /// The data to parse.
+ /// The client options for reading and writing models.
+ protected override Animal PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ switch (format)
+ {
+ case "J":
+ using (JsonDocument document = JsonDocument.Parse(data))
+ {
+ return DeserializePet(document.RootElement, options);
+ }
+ default:
+ throw new FormatException($"The model {nameof(Pet)} does not support reading '{options.Format}' format.");
+ }
+ }
+
+ /// The client options for reading and writing models.
+ string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "J";
+
+ /// The to serialize into .
+ public static implicit operator BinaryContent(Pet pet)
+ {
+ if (pet == null)
+ {
+ return null;
+ }
+ return BinaryContent.Create(pet, ModelSerializationExtensions.WireOptions);
+ }
+
+ /// The to deserialize the from.
+ public static explicit operator Pet(ClientResult result)
+ {
+ using PipelineResponse response = result.GetRawResponse();
+ using JsonDocument document = JsonDocument.Parse(response.Content);
+ return DeserializePet(document.RootElement, ModelSerializationExtensions.WireOptions);
+ }
+ }
+}
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/Pet.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/Pet.cs
new file mode 100644
index 00000000000..fd7e8130718
--- /dev/null
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/Pet.cs
@@ -0,0 +1,37 @@
+//
+
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+
+namespace SampleTypeSpec
+{
+ /// Pet is a discriminated animal.
+ public partial class Pet : Animal
+ {
+ /// Initializes a new instance of .
+ /// Name of the animal.
+ /// Whether the pet is trained.
+ /// is null.
+ public Pet(string name, bool trained) : base("pet", name)
+ {
+ Argument.AssertNotNull(name, nameof(name));
+
+ Trained = trained;
+ }
+
+ /// Initializes a new instance of .
+ /// The kind of animal.
+ /// Name of the animal.
+ /// Keeps track of any properties unknown to the library.
+ /// Whether the pet is trained.
+ internal Pet(string kind, string name, IDictionary additionalBinaryDataProperties, bool trained) : base(kind, name, additionalBinaryDataProperties)
+ {
+ Trained = trained;
+ }
+
+ /// Whether the pet is trained.
+ public bool Trained { get; set; }
+ }
+}
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/SampleTypeSpecContext.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/SampleTypeSpecContext.cs
index ca0a9f6991a..acbd82d2f33 100644
--- a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/SampleTypeSpecContext.cs
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/SampleTypeSpecContext.cs
@@ -10,7 +10,9 @@ namespace SampleTypeSpec
/// Context class which will be filled in by the System.ClientModel.SourceGeneration.
/// For more information
///
+ [ModelReaderWriterBuildable(typeof(Animal))]
[ModelReaderWriterBuildable(typeof(AnotherDynamicModel))]
+ [ModelReaderWriterBuildable(typeof(Dog))]
[ModelReaderWriterBuildable(typeof(DynamicModel))]
[ModelReaderWriterBuildable(typeof(Friend))]
[ModelReaderWriterBuildable(typeof(GetWidgetMetricsResponse))]
@@ -21,10 +23,13 @@ namespace SampleTypeSpec
[ModelReaderWriterBuildable(typeof(ModelWithEmbeddedNonBodyParameters))]
[ModelReaderWriterBuildable(typeof(ModelWithRequiredNullableProperties))]
[ModelReaderWriterBuildable(typeof(PageThing))]
+ [ModelReaderWriterBuildable(typeof(Pet))]
[ModelReaderWriterBuildable(typeof(RenamedModel))]
[ModelReaderWriterBuildable(typeof(ReturnsAnonymousModelResponse))]
[ModelReaderWriterBuildable(typeof(RoundTripModel))]
[ModelReaderWriterBuildable(typeof(Thing))]
+ [ModelReaderWriterBuildable(typeof(UnknownAnimal))]
+ [ModelReaderWriterBuildable(typeof(UnknownPet))]
public partial class SampleTypeSpecContext : ModelReaderWriterContext
{
}
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/UnknownAnimal.Serialization.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/UnknownAnimal.Serialization.cs
new file mode 100644
index 00000000000..e00de335fb2
--- /dev/null
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/UnknownAnimal.Serialization.cs
@@ -0,0 +1,128 @@
+//
+
+#nullable disable
+
+using System;
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+using System.Text.Json;
+
+namespace SampleTypeSpec
+{
+ internal partial class UnknownAnimal : Animal, IJsonModel
+ {
+ /// Initializes a new instance of for deserialization.
+ internal UnknownAnimal()
+ {
+ }
+
+ /// The JSON writer.
+ /// The client options for reading and writing models.
+ void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options)
+ {
+ writer.WriteStartObject();
+ JsonModelWriteCore(writer, options);
+ writer.WriteEndObject();
+ }
+
+ /// The JSON writer.
+ /// The client options for reading and writing models.
+ protected override void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ if (format != "J")
+ {
+ throw new FormatException($"The model {nameof(Animal)} does not support writing '{format}' format.");
+ }
+ base.JsonModelWriteCore(writer, options);
+ }
+
+ /// The JSON reader.
+ /// The client options for reading and writing models.
+ Animal IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) => JsonModelCreateCore(ref reader, options);
+
+ /// The JSON reader.
+ /// The client options for reading and writing models.
+ protected override Animal JsonModelCreateCore(ref Utf8JsonReader reader, ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ if (format != "J")
+ {
+ throw new FormatException($"The model {nameof(Animal)} does not support reading '{format}' format.");
+ }
+ using JsonDocument document = JsonDocument.ParseValue(ref reader);
+ return DeserializeAnimal(document.RootElement, options);
+ }
+
+ /// The JSON element to deserialize.
+ /// The client options for reading and writing models.
+ internal static UnknownAnimal DeserializeUnknownAnimal(JsonElement element, ModelReaderWriterOptions options)
+ {
+ if (element.ValueKind == JsonValueKind.Null)
+ {
+ return null;
+ }
+ string kind = "unknown";
+ string name = default;
+ IDictionary additionalBinaryDataProperties = new ChangeTrackingDictionary();
+ foreach (var prop in element.EnumerateObject())
+ {
+ if (prop.NameEquals("kind"u8))
+ {
+ kind = prop.Value.GetString();
+ continue;
+ }
+ if (prop.NameEquals("name"u8))
+ {
+ name = prop.Value.GetString();
+ continue;
+ }
+ if (options.Format != "W")
+ {
+ additionalBinaryDataProperties.Add(prop.Name, BinaryData.FromString(prop.Value.GetRawText()));
+ }
+ }
+ return new UnknownAnimal(kind, name, additionalBinaryDataProperties);
+ }
+
+ /// The client options for reading and writing models.
+ BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options);
+
+ /// The client options for reading and writing models.
+ protected override BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ switch (format)
+ {
+ case "J":
+ return ModelReaderWriter.Write(this, options, SampleTypeSpecContext.Default);
+ default:
+ throw new FormatException($"The model {nameof(Animal)} does not support writing '{options.Format}' format.");
+ }
+ }
+
+ /// The data to parse.
+ /// The client options for reading and writing models.
+ Animal IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => PersistableModelCreateCore(data, options);
+
+ /// The data to parse.
+ /// The client options for reading and writing models.
+ protected override Animal PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ switch (format)
+ {
+ case "J":
+ using (JsonDocument document = JsonDocument.Parse(data))
+ {
+ return DeserializeAnimal(document.RootElement, options);
+ }
+ default:
+ throw new FormatException($"The model {nameof(Animal)} does not support reading '{options.Format}' format.");
+ }
+ }
+
+ /// The client options for reading and writing models.
+ string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "J";
+ }
+}
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/UnknownAnimal.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/UnknownAnimal.cs
new file mode 100644
index 00000000000..6b283549666
--- /dev/null
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/UnknownAnimal.cs
@@ -0,0 +1,20 @@
+//
+
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+
+namespace SampleTypeSpec
+{
+ internal partial class UnknownAnimal : Animal
+ {
+ /// Initializes a new instance of .
+ /// The kind of animal.
+ /// Name of the animal.
+ /// Keeps track of any properties unknown to the library.
+ internal UnknownAnimal(string kind, string name, IDictionary additionalBinaryDataProperties) : base(kind ?? "unknown", name, additionalBinaryDataProperties)
+ {
+ }
+ }
+}
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/UnknownPet.Serialization.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/UnknownPet.Serialization.cs
new file mode 100644
index 00000000000..59f22d181dc
--- /dev/null
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/UnknownPet.Serialization.cs
@@ -0,0 +1,134 @@
+//
+
+#nullable disable
+
+using System;
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+using System.Text.Json;
+
+namespace SampleTypeSpec
+{
+ internal partial class UnknownPet : Pet, IJsonModel
+ {
+ /// Initializes a new instance of for deserialization.
+ internal UnknownPet()
+ {
+ }
+
+ /// The JSON writer.
+ /// The client options for reading and writing models.
+ void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options)
+ {
+ writer.WriteStartObject();
+ JsonModelWriteCore(writer, options);
+ writer.WriteEndObject();
+ }
+
+ /// The JSON writer.
+ /// The client options for reading and writing models.
+ protected override void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ if (format != "J")
+ {
+ throw new FormatException($"The model {nameof(Pet)} does not support writing '{format}' format.");
+ }
+ base.JsonModelWriteCore(writer, options);
+ }
+
+ /// The JSON reader.
+ /// The client options for reading and writing models.
+ Pet IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) => (UnknownPet)JsonModelCreateCore(ref reader, options);
+
+ /// The JSON reader.
+ /// The client options for reading and writing models.
+ protected override Animal JsonModelCreateCore(ref Utf8JsonReader reader, ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ if (format != "J")
+ {
+ throw new FormatException($"The model {nameof(Pet)} does not support reading '{format}' format.");
+ }
+ using JsonDocument document = JsonDocument.ParseValue(ref reader);
+ return DeserializePet(document.RootElement, options);
+ }
+
+ /// The JSON element to deserialize.
+ /// The client options for reading and writing models.
+ internal static UnknownPet DeserializeUnknownPet(JsonElement element, ModelReaderWriterOptions options)
+ {
+ if (element.ValueKind == JsonValueKind.Null)
+ {
+ return null;
+ }
+ string kind = "unknown";
+ string name = default;
+ IDictionary additionalBinaryDataProperties = new ChangeTrackingDictionary();
+ bool trained = default;
+ foreach (var prop in element.EnumerateObject())
+ {
+ if (prop.NameEquals("kind"u8))
+ {
+ kind = prop.Value.GetString();
+ continue;
+ }
+ if (prop.NameEquals("name"u8))
+ {
+ name = prop.Value.GetString();
+ continue;
+ }
+ if (prop.NameEquals("trained"u8))
+ {
+ trained = prop.Value.GetBoolean();
+ continue;
+ }
+ if (options.Format != "W")
+ {
+ additionalBinaryDataProperties.Add(prop.Name, BinaryData.FromString(prop.Value.GetRawText()));
+ }
+ }
+ return new UnknownPet(kind, name, additionalBinaryDataProperties, trained);
+ }
+
+ /// The client options for reading and writing models.
+ BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options);
+
+ /// The client options for reading and writing models.
+ protected override BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ switch (format)
+ {
+ case "J":
+ return ModelReaderWriter.Write(this, options, SampleTypeSpecContext.Default);
+ default:
+ throw new FormatException($"The model {nameof(Pet)} does not support writing '{options.Format}' format.");
+ }
+ }
+
+ /// The data to parse.
+ /// The client options for reading and writing models.
+ Pet IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => (UnknownPet)PersistableModelCreateCore(data, options);
+
+ /// The data to parse.
+ /// The client options for reading and writing models.
+ protected override Animal PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options)
+ {
+ string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format;
+ switch (format)
+ {
+ case "J":
+ using (JsonDocument document = JsonDocument.Parse(data))
+ {
+ return DeserializePet(document.RootElement, options);
+ }
+ default:
+ throw new FormatException($"The model {nameof(Pet)} does not support reading '{options.Format}' format.");
+ }
+ }
+
+ /// The client options for reading and writing models.
+ string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "J";
+ }
+}
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/UnknownPet.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/UnknownPet.cs
new file mode 100644
index 00000000000..d0cd927230b
--- /dev/null
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/Models/UnknownPet.cs
@@ -0,0 +1,21 @@
+//
+
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+
+namespace SampleTypeSpec
+{
+ internal partial class UnknownPet : Pet
+ {
+ /// Initializes a new instance of .
+ /// The kind of animal.
+ /// Name of the animal.
+ /// Keeps track of any properties unknown to the library.
+ /// Whether the pet is trained.
+ internal UnknownPet(string kind, string name, IDictionary additionalBinaryDataProperties, bool trained) : base(kind ?? "unknown", name, additionalBinaryDataProperties, trained)
+ {
+ }
+ }
+}
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/PetOperations.RestClient.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/PetOperations.RestClient.cs
new file mode 100644
index 00000000000..e39efd2c1cf
--- /dev/null
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/PetOperations.RestClient.cs
@@ -0,0 +1,45 @@
+//
+
+#nullable disable
+
+using System.ClientModel;
+using System.ClientModel.Primitives;
+
+namespace SampleTypeSpec
+{
+ ///
+ public partial class PetOperations
+ {
+ private static PipelineMessageClassifier _pipelineMessageClassifier200;
+
+ private static PipelineMessageClassifier PipelineMessageClassifier200 => _pipelineMessageClassifier200 = PipelineMessageClassifier.Create(stackalloc ushort[] { 200 });
+
+ internal PipelineMessage CreateUpdatePetAsPetRequest(BinaryContent content, RequestOptions options)
+ {
+ ClientUriBuilder uri = new ClientUriBuilder();
+ uri.Reset(_endpoint);
+ uri.AppendPath("/pets/pet/as-pet", false);
+ PipelineMessage message = Pipeline.CreateMessage(uri.ToUri(), "PUT", PipelineMessageClassifier200);
+ PipelineRequest request = message.Request;
+ request.Headers.Set("Content-Type", "application/json");
+ request.Headers.Set("Accept", "application/json");
+ request.Content = content;
+ message.Apply(options);
+ return message;
+ }
+
+ internal PipelineMessage CreateUpdateDogAsPetRequest(BinaryContent content, RequestOptions options)
+ {
+ ClientUriBuilder uri = new ClientUriBuilder();
+ uri.Reset(_endpoint);
+ uri.AppendPath("/pets/dog/as-pet", false);
+ PipelineMessage message = Pipeline.CreateMessage(uri.ToUri(), "PUT", PipelineMessageClassifier200);
+ PipelineRequest request = message.Request;
+ request.Headers.Set("Content-Type", "application/json");
+ request.Headers.Set("Accept", "application/json");
+ request.Content = content;
+ message.Apply(options);
+ return message;
+ }
+ }
+}
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/PetOperations.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/PetOperations.cs
new file mode 100644
index 00000000000..2a60348d6b5
--- /dev/null
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/PetOperations.cs
@@ -0,0 +1,275 @@
+//
+
+#nullable disable
+
+using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SampleTypeSpec
+{
+ /// The PetOperations sub-client.
+ public partial class PetOperations
+ {
+ private readonly Uri _endpoint;
+
+ /// Initializes a new instance of PetOperations for mocking.
+ protected PetOperations()
+ {
+ }
+
+ /// Initializes a new instance of PetOperations.
+ /// The HTTP pipeline for sending and receiving REST requests and responses.
+ /// Service endpoint.
+ internal PetOperations(ClientPipeline pipeline, Uri endpoint)
+ {
+ _endpoint = endpoint;
+ Pipeline = pipeline;
+ }
+
+ /// The HTTP pipeline for sending and receiving REST requests and responses.
+ public ClientPipeline Pipeline { get; }
+
+ ///
+ /// [Protocol Method] Update a pet as a pet
+ ///
+ /// -
+ /// This protocol method allows explicit creation of the request and processing of the response for advanced scenarios.
+ ///
+ ///
+ ///
+ /// The content to send as the body of the request.
+ /// The request options, which can override default behaviors of the client pipeline on a per-call basis.
+ /// is null.
+ /// Service returned a non-success status code.
+ /// The response returned from the service.
+ public virtual ClientResult UpdatePetAsPet(BinaryContent content, RequestOptions options = null)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdatePetAsPet.");
+ Argument.AssertNotNull(content, nameof(content));
+
+ using PipelineMessage message = CreateUpdatePetAsPetRequest(content, options);
+ return ClientResult.FromResponse(Pipeline.ProcessMessage(message, options));
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdatePetAsPet: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdatePetAsPet.");
+ }
+ }
+
+ ///
+ /// [Protocol Method] Update a pet as a pet
+ ///
+ /// -
+ /// This protocol method allows explicit creation of the request and processing of the response for advanced scenarios.
+ ///
+ ///
+ ///
+ /// The content to send as the body of the request.
+ /// The request options, which can override default behaviors of the client pipeline on a per-call basis.
+ /// is null.
+ /// Service returned a non-success status code.
+ /// The response returned from the service.
+ public virtual async Task UpdatePetAsPetAsync(BinaryContent content, RequestOptions options = null)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdatePetAsPetAsync.");
+ Argument.AssertNotNull(content, nameof(content));
+
+ using PipelineMessage message = CreateUpdatePetAsPetRequest(content, options);
+ return ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false));
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdatePetAsPetAsync: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdatePetAsPetAsync.");
+ }
+ }
+
+ /// Update a pet as a pet.
+ ///
+ /// The cancellation token that can be used to cancel the operation.
+ /// is null.
+ /// Service returned a non-success status code.
+ public virtual ClientResult UpdatePetAsPet(Pet pet, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdatePetAsPet.");
+ Argument.AssertNotNull(pet, nameof(pet));
+
+ ClientResult result = UpdatePetAsPet(pet, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null);
+ return ClientResult.FromValue((Pet)result, result.GetRawResponse());
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdatePetAsPet: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdatePetAsPet.");
+ }
+ }
+
+ /// Update a pet as a pet.
+ ///
+ /// The cancellation token that can be used to cancel the operation.
+ /// is null.
+ /// Service returned a non-success status code.
+ public virtual async Task> UpdatePetAsPetAsync(Pet pet, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdatePetAsPetAsync.");
+ Argument.AssertNotNull(pet, nameof(pet));
+
+ ClientResult result = await UpdatePetAsPetAsync(pet, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false);
+ return ClientResult.FromValue((Pet)result, result.GetRawResponse());
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdatePetAsPetAsync: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdatePetAsPetAsync.");
+ }
+ }
+
+ ///
+ /// [Protocol Method] Update a dog as a pet
+ ///
+ /// -
+ /// This protocol method allows explicit creation of the request and processing of the response for advanced scenarios.
+ ///
+ ///
+ ///
+ /// The content to send as the body of the request.
+ /// The request options, which can override default behaviors of the client pipeline on a per-call basis.
+ /// is null.
+ /// Service returned a non-success status code.
+ /// The response returned from the service.
+ public virtual ClientResult UpdateDogAsPet(BinaryContent content, RequestOptions options = null)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdateDogAsPet.");
+ Argument.AssertNotNull(content, nameof(content));
+
+ using PipelineMessage message = CreateUpdateDogAsPetRequest(content, options);
+ return ClientResult.FromResponse(Pipeline.ProcessMessage(message, options));
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdateDogAsPet: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdateDogAsPet.");
+ }
+ }
+
+ ///
+ /// [Protocol Method] Update a dog as a pet
+ ///
+ /// -
+ /// This protocol method allows explicit creation of the request and processing of the response for advanced scenarios.
+ ///
+ ///
+ ///
+ /// The content to send as the body of the request.
+ /// The request options, which can override default behaviors of the client pipeline on a per-call basis.
+ /// is null.
+ /// Service returned a non-success status code.
+ /// The response returned from the service.
+ public virtual async Task UpdateDogAsPetAsync(BinaryContent content, RequestOptions options = null)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdateDogAsPetAsync.");
+ Argument.AssertNotNull(content, nameof(content));
+
+ using PipelineMessage message = CreateUpdateDogAsPetRequest(content, options);
+ return ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false));
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdateDogAsPetAsync: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdateDogAsPetAsync.");
+ }
+ }
+
+ /// Update a dog as a pet.
+ ///
+ /// The cancellation token that can be used to cancel the operation.
+ /// is null.
+ /// Service returned a non-success status code.
+ public virtual ClientResult UpdateDogAsPet(Pet pet, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdateDogAsPet.");
+ Argument.AssertNotNull(pet, nameof(pet));
+
+ ClientResult result = UpdateDogAsPet(pet, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null);
+ return ClientResult.FromValue((Pet)result, result.GetRawResponse());
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdateDogAsPet: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdateDogAsPet.");
+ }
+ }
+
+ /// Update a dog as a pet.
+ ///
+ /// The cancellation token that can be used to cancel the operation.
+ /// is null.
+ /// Service returned a non-success status code.
+ public virtual async Task> UpdateDogAsPetAsync(Pet pet, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ System.Console.WriteLine("Entering method UpdateDogAsPetAsync.");
+ Argument.AssertNotNull(pet, nameof(pet));
+
+ ClientResult result = await UpdateDogAsPetAsync(pet, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false);
+ return ClientResult.FromValue((Pet)result, result.GetRawResponse());
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"An exception was thrown in method UpdateDogAsPetAsync: {ex}");
+ throw;
+ }
+ finally
+ {
+ System.Console.WriteLine("Exiting method UpdateDogAsPetAsync.");
+ }
+ }
+ }
+}
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/SampleTypeSpecClient.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/SampleTypeSpecClient.cs
index 243a0d06d09..812e2900259 100644
--- a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/SampleTypeSpecClient.cs
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/SampleTypeSpecClient.cs
@@ -32,6 +32,9 @@ public partial class SampleTypeSpecClient
}
};
private readonly string _apiVersion;
+ private AnimalOperations _cachedAnimalOperations;
+ private PetOperations _cachedPetOperations;
+ private DogOperations _cachedDogOperations;
private Metrics _cachedMetrics;
/// Initializes a new instance of SampleTypeSpecClient for mocking.
@@ -3008,6 +3011,24 @@ public virtual async Task DynamicModelOperationAsync(DynamicModel
}
}
+ /// Initializes a new instance of AnimalOperations.
+ public virtual AnimalOperations GetAnimalOperationsClient()
+ {
+ return Volatile.Read(ref _cachedAnimalOperations) ?? Interlocked.CompareExchange(ref _cachedAnimalOperations, new AnimalOperations(Pipeline, _endpoint), null) ?? _cachedAnimalOperations;
+ }
+
+ /// Initializes a new instance of PetOperations.
+ public virtual PetOperations GetPetOperationsClient()
+ {
+ return Volatile.Read(ref _cachedPetOperations) ?? Interlocked.CompareExchange(ref _cachedPetOperations, new PetOperations(Pipeline, _endpoint), null) ?? _cachedPetOperations;
+ }
+
+ /// Initializes a new instance of DogOperations.
+ public virtual DogOperations GetDogOperationsClient()
+ {
+ return Volatile.Read(ref _cachedDogOperations) ?? Interlocked.CompareExchange(ref _cachedDogOperations, new DogOperations(Pipeline, _endpoint), null) ?? _cachedDogOperations;
+ }
+
/// Initializes a new instance of Metrics.
public virtual Metrics GetMetricsClient()
{
diff --git a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/SampleTypeSpecModelFactory.cs b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/SampleTypeSpecModelFactory.cs
index 2d1f276ad6d..370465cb29d 100644
--- a/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/SampleTypeSpecModelFactory.cs
+++ b/docs/samples/client/csharp/SampleService/SampleClient/src/Generated/SampleTypeSpecModelFactory.cs
@@ -230,6 +230,37 @@ public static AnotherDynamicModel AnotherDynamicModel(string bar = default)
return new AnotherDynamicModel(bar, additionalBinaryDataProperties: null);
}
+ ///
+ /// Base animal with discriminator
+ /// Please note this is the abstract base class. The derived classes available for instantiation are: and .
+ ///
+ /// The kind of animal.
+ /// Name of the animal.
+ /// A new instance for mocking.
+ public static Animal Animal(string kind = default, string name = default)
+ {
+ return new UnknownAnimal(kind, name, additionalBinaryDataProperties: null);
+ }
+
+ /// Pet is a discriminated animal.
+ /// Name of the animal.
+ /// Whether the pet is trained.
+ /// A new instance for mocking.
+ public static Pet Pet(string name = default, bool trained = default)
+ {
+ return new Pet("pet", name, additionalBinaryDataProperties: null, trained);
+ }
+
+ /// Dog is a specific type of pet with hierarchy building.
+ /// Name of the animal.
+ /// Whether the pet is trained.
+ /// The breed of the dog.
+ /// A new instance for mocking.
+ public static Dog Dog(string name = default, bool trained = default, string breed = default)
+ {
+ return new Dog("pet", name, additionalBinaryDataProperties: null, trained, breed);
+ }
+
/// The GetWidgetMetricsResponse.
///
///
diff --git a/docs/samples/client/csharp/SampleService/main.tsp b/docs/samples/client/csharp/SampleService/main.tsp
index 6352a85e816..3a6a1e6e254 100644
--- a/docs/samples/client/csharp/SampleService/main.tsp
+++ b/docs/samples/client/csharp/SampleService/main.tsp
@@ -502,6 +502,74 @@ op EmbeddedParameters(@bodyRoot body: ModelWithEmbeddedNonBodyParameters): void;
@post
op DynamicModelOperation(@body body: DynamicModel): void;
+@doc("Base animal with discriminator")
+@discriminator("kind")
+model Animal {
+ @doc("The kind of animal")
+ kind: string;
+
+ @doc("Name of the animal")
+ name: string;
+}
+
+alias PetContent = {
+ @doc("Whether the pet is trained")
+ trained: boolean;
+};
+
+@doc("Pet is a discriminated animal")
+model Pet extends Animal {
+ kind: "pet";
+ ...PetContent;
+}
+
+alias DogContent = {
+ @doc("The breed of the dog")
+ breed: string;
+};
+
+@doc("Dog is a specific type of pet with hierarchy building")
+@Legacy.hierarchyBuilding(Pet)
+model Dog extends Animal {
+ kind: "dog";
+ ...PetContent;
+ ...DogContent;
+}
+
+@route("/animals")
+interface AnimalOperations {
+ @doc("Update a pet as an animal")
+ @put
+ @route("/pet/as-animal")
+ updatePetAsAnimal(@body animal: Animal): Animal;
+
+ @doc("Update a dog as an animal")
+ @put
+ @route("/dog/as-animal")
+ updateDogAsAnimal(@body animal: Animal): Animal;
+}
+
+@route("/pets")
+interface PetOperations {
+ @doc("Update a pet as a pet")
+ @put
+ @route("/pet/as-pet")
+ updatePetAsPet(@body pet: Pet): Pet;
+
+ @doc("Update a dog as a pet")
+ @put
+ @route("/dog/as-pet")
+ updateDogAsPet(@body pet: Pet): Pet;
+}
+
+@route("/dogs")
+interface DogOperations {
+ @doc("Update a dog as a dog")
+ @put
+ @route("/dog/as-dog")
+ updateDogAsDog(@body dog: Dog): Dog;
+}
+
@clientInitialization({
initializedBy: InitializedBy.individually | InitializedBy.parent,
})
diff --git a/eng/common/pipelines/templates/variables/image.yml b/eng/common/pipelines/templates/variables/image.yml
index 8c0b780abc1..87d9439f813 100644
--- a/eng/common/pipelines/templates/variables/image.yml
+++ b/eng/common/pipelines/templates/variables/image.yml
@@ -17,15 +17,15 @@ variables:
- name: LINUXNEXTVMIMAGE
value: ubuntu-24.04
- name: LINUXARMVMIMAGE
- value: azsdk-pool-mms-mariner-2-arm-1espt
+ value: azure-linux-3-arm64
- name: WINDOWSVMIMAGE
value: windows-2022
- name: WINDOWSARMVMIMAGE
value: windows-2022-arm64-1espt
- - name: MACVMIMAGE # mac - arm64
- value: macos-latest
- - name: MACVMIMAGE13 # mac - x64
- value: macos-13
+ - name: MAC_ARM_VM_IMAGE # mac - arm64
+ value: macos-latest-internal
+ - name: MAC_X64_VM_IMAGE # mac - x64
+ value: macos-15
# Values required for pool.os field in 1es pipeline templates. Variable form
# cannot be used, instead those values must be written directly into pool.os.
diff --git a/eng/tsp-core/pipelines/jobs/cli/build-tsp-cli.yml b/eng/tsp-core/pipelines/jobs/cli/build-tsp-cli.yml
index 41e79000ca7..da0d39ebed9 100644
--- a/eng/tsp-core/pipelines/jobs/cli/build-tsp-cli.yml
+++ b/eng/tsp-core/pipelines/jobs/cli/build-tsp-cli.yml
@@ -17,11 +17,11 @@ jobs:
hostArchitecture: arm64
${{ if eq(parameters.platform, 'macos-x64') }}:
name: $(MACPOOL)
- vmImage: $(MACVMIMAGE13)
+ vmImage: $(MAC_X64_VM_IMAGE)
os: macOS
${{ if eq(parameters.platform, 'macos-arm64') }}:
name: $(MACPOOL)
- vmImage: $(MACVMIMAGE)
+ vmImage: $(MAC_ARM_VM_IMAGE)
os: macOS
${{ if eq(parameters.platform, 'windows-x64') }}:
name: $(WINDOWSPOOL)
diff --git a/eng/tsp-core/pipelines/jobs/cli/verify-tsp-cli.yml b/eng/tsp-core/pipelines/jobs/cli/verify-tsp-cli.yml
index 566ede09b30..8551b416fd2 100644
--- a/eng/tsp-core/pipelines/jobs/cli/verify-tsp-cli.yml
+++ b/eng/tsp-core/pipelines/jobs/cli/verify-tsp-cli.yml
@@ -23,11 +23,11 @@ jobs:
hostArchitecture: arm64
${{ if eq(parameters.platform, 'macos-x64') }}:
name: $(MACPOOL)
- vmImage: $(MACVMIMAGE13)
+ vmImage: $(MAC_X64_VM_IMAGE)
os: macOS
${{ if eq(parameters.platform, 'macos-arm64') }}:
name: $(MACPOOL)
- vmImage: $(MACVMIMAGE)
+ vmImage: $(MAC_ARM_VM_IMAGE)
os: macOS
${{ if eq(parameters.platform, 'windows-x64') }}:
name: $(WINDOWSPOOL)
diff --git a/eng/tsp-core/pipelines/publish.yml b/eng/tsp-core/pipelines/publish.yml
index ab6e626181d..30c8b574c29 100644
--- a/eng/tsp-core/pipelines/publish.yml
+++ b/eng/tsp-core/pipelines/publish.yml
@@ -6,7 +6,6 @@ trigger:
- main
# For patch releases
- release/*
- paths:
exclude:
- packages/http-client-csharp
diff --git a/packages/asset-emitter/CHANGELOG.md b/packages/asset-emitter/CHANGELOG.md
index 9abff680b7e..b6c86653f28 100644
--- a/packages/asset-emitter/CHANGELOG.md
+++ b/packages/asset-emitter/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog - @typespec/asset-emitter
+## 0.77.0
+
+### Bump dependencies
+
+- [#9046](https://github.com/microsoft/typespec/pull/9046) Upgrade dependencies
+
+
## 0.76.0
### Bump dependencies
diff --git a/packages/asset-emitter/package.json b/packages/asset-emitter/package.json
index c928d460fa9..bb6116615a5 100644
--- a/packages/asset-emitter/package.json
+++ b/packages/asset-emitter/package.json
@@ -1,6 +1,6 @@
{
"name": "@typespec/asset-emitter",
- "version": "0.76.0",
+ "version": "0.77.0",
"author": "Microsoft Corporation",
"description": "TypeSpec Asset Emitter, this is to be replaced by the new emitter framework",
"homepage": "https://typespec.io",
diff --git a/packages/astro-utils/package.json b/packages/astro-utils/package.json
index 8a7b2ed075a..4984ea66775 100644
--- a/packages/astro-utils/package.json
+++ b/packages/astro-utils/package.json
@@ -24,14 +24,14 @@
},
"devDependencies": {
"@types/react": "~19.2.2",
- "astro": "^5.5.6"
+ "astro": "^5.16.4"
},
"peerDependencies": {
- "astro": "^5.5.6"
+ "astro": "^5.16.4"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
- "@astrojs/starlight": "^0.36.1",
+ "@astrojs/starlight": "^0.37.0",
"@expressive-code/core": "^0.41.2",
"@typespec/playground": "workspace:^",
"astro-expressive-code": "^0.41.2",
diff --git a/packages/bundler/CHANGELOG.md b/packages/bundler/CHANGELOG.md
index ebb1621c840..bb02ab580b7 100644
--- a/packages/bundler/CHANGELOG.md
+++ b/packages/bundler/CHANGELOG.md
@@ -1,5 +1,12 @@
# Change Log - @typespec/bundler
+## 0.4.6
+
+### Bump dependencies
+
+- [#9046](https://github.com/microsoft/typespec/pull/9046) Upgrade dependencies
+
+
## 0.4.5
### Bump dependencies
diff --git a/packages/bundler/package.json b/packages/bundler/package.json
index 9df27e4475d..b9512871172 100644
--- a/packages/bundler/package.json
+++ b/packages/bundler/package.json
@@ -1,6 +1,6 @@
{
"name": "@typespec/bundler",
- "version": "0.4.5",
+ "version": "0.4.6",
"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 c012937c02d..2b55e46d03d 100644
--- a/packages/compiler/CHANGELOG.md
+++ b/packages/compiler/CHANGELOG.md
@@ -1,5 +1,32 @@
# Change Log - @typespec/compiler
+## 1.7.1
+
+### Bug Fixes
+
+- [#9210](https://github.com/microsoft/typespec/pull/9210) Fix crash in `tsp init` introduced in `1.7.0`
+
+
+## 1.7.0
+
+### Features
+
+- [#9002](https://github.com/microsoft/typespec/pull/9002) Add `commaDelimited` and `newlineDelimited` values to `ArrayEncoding` enum for serializing arrays with comma and newline delimiters
+- [#8942](https://github.com/microsoft/typespec/pull/8942) - Add 'exit' final event for linter rules
+ - Support 'async' in linter definition and async function as callback for 'exit' event.
+- [#9024](https://github.com/microsoft/typespec/pull/9024) [API] Add `node` to `SourceModel` type
+- [#8619](https://github.com/microsoft/typespec/pull/8619) Add support for escaping param like tags(`@param`, `@prop`, etc.) identifier with backtick in doc comments to allow special characters
+
+### Bump dependencies
+
+- [#9046](https://github.com/microsoft/typespec/pull/9046) Upgrade dependencies
+
+### Bug Fixes
+
+- [#8917](https://github.com/microsoft/typespec/pull/8917) Add security warning to tsp init CLI documentation for external templates (#8916)
+- [#8997](https://github.com/microsoft/typespec/pull/8997) UnusedUsing Diagnostics are reported as warning instead of hint when there are linters defined in tspconfig.yaml
+
+
## 1.6.0
### Features
diff --git a/packages/compiler/package.json b/packages/compiler/package.json
index 93c0b36d9be..a336aa71158 100644
--- a/packages/compiler/package.json
+++ b/packages/compiler/package.json
@@ -1,6 +1,6 @@
{
"name": "@typespec/compiler",
- "version": "1.6.0",
+ "version": "1.7.1",
"description": "TypeSpec Compiler Preview",
"author": "Microsoft Corporation",
"license": "MIT",
diff --git a/packages/compiler/src/config/config-schema.ts b/packages/compiler/src/config/config-schema.ts
index 0df1c8b9333..d1705f46947 100644
--- a/packages/compiler/src/config/config-schema.ts
+++ b/packages/compiler/src/config/config-schema.ts
@@ -103,5 +103,30 @@ export const TypeSpecConfigJsonSchema: JSONSchemaType = {
},
},
} as any, // ajv type system doesn't like the string templates
+ transformer: {
+ type: "object",
+ nullable: true,
+ required: [],
+ additionalProperties: false,
+ properties: {
+ extends: {
+ type: "array",
+ nullable: true,
+ items: { type: "string" },
+ },
+ enable: {
+ type: "object",
+ required: [],
+ nullable: true,
+ additionalProperties: { type: "boolean" },
+ },
+ disable: {
+ type: "object",
+ required: [],
+ nullable: true,
+ additionalProperties: { type: "string" },
+ },
+ },
+ } as any, // ajv type system doesn't like the string templates
},
};
diff --git a/packages/compiler/src/config/types.ts b/packages/compiler/src/config/types.ts
index f38d07a1173..e6c76c907b3 100644
--- a/packages/compiler/src/config/types.ts
+++ b/packages/compiler/src/config/types.ts
@@ -1,4 +1,4 @@
-import type { Diagnostic, RuleRef } from "../core/types.js";
+import type { Diagnostic, RuleRef, TransformSetRef } from "../core/types.js";
import type { YamlScript } from "../yaml/types.js";
/**
@@ -69,6 +69,8 @@ export interface TypeSpecConfig {
options?: Record;
linter?: LinterConfig;
+
+ transformer?: TransformerConfig;
}
/**
@@ -88,6 +90,7 @@ export interface TypeSpecRawConfig {
options?: Record;
linter?: LinterConfig;
+ transformer?: TransformerConfig;
}
export interface ConfigEnvironmentVariable {
@@ -107,3 +110,9 @@ export interface LinterConfig {
enable?: Record;
disable?: Record;
}
+
+export interface TransformerConfig {
+ extends?: TransformSetRef[];
+ enable?: Record;
+ disable?: Record;
+}
diff --git a/packages/compiler/src/core/cli/cli.ts b/packages/compiler/src/core/cli/cli.ts
index 200b37502b8..80c85892d2d 100644
--- a/packages/compiler/src/core/cli/cli.ts
+++ b/packages/compiler/src/core/cli/cli.ts
@@ -211,7 +211,8 @@ async function main() {
(cmd) =>
cmd
.positional("templatesUrl", {
- description: "Url of the initialization template",
+ description:
+ "Url of the initialization template. WARNING: Downloading or using an untrusted template may contain malicious packages that can compromise your system and data. Proceed with caution and verify the source.",
type: "string",
})
.option("template", {
diff --git a/packages/compiler/src/core/library.ts b/packages/compiler/src/core/library.ts
index 24687c6f774..882400b8283 100644
--- a/packages/compiler/src/core/library.ts
+++ b/packages/compiler/src/core/library.ts
@@ -9,6 +9,8 @@ import {
LinterRuleDefinition,
PackageFlags,
StateDef,
+ TransformDefinition,
+ TransformerDefinition,
TypeSpecLibrary,
TypeSpecLibraryDef,
} from "./types.js";
@@ -116,6 +118,16 @@ export function createLinterRule(definition: TransformDefinition) {
+ compilerAssert(!definition.name.includes("/"), "Transform name cannot contain a '/'.");
+ return definition;
+}
+
/**
* Set the TypeSpec namespace for that function.
* @param namespace Namespace string (e.g. "Foo.Bar")
diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts
index 5c42d275d59..7b580f2566d 100644
--- a/packages/compiler/src/core/messages.ts
+++ b/packages/compiler/src/core/messages.ts
@@ -761,6 +761,40 @@ const diagnostics = {
},
},
+ /**
+ * Transformer
+ */
+ "invalid-transform-ref": {
+ severity: "error",
+ messages: {
+ default: paramMessage`Reference "${"ref"}" is not a valid reference to a transform or transform set. It must be in the following format: "/"`,
+ },
+ },
+ "unknown-transform": {
+ severity: "warning",
+ messages: {
+ default: paramMessage`Transform "${"transformName"}" is not found in library "${"libraryName"}"`,
+ },
+ },
+ "unknown-transform-set": {
+ severity: "warning",
+ messages: {
+ default: paramMessage`Transform set "${"transformSetName"}" is not found in library "${"libraryName"}"`,
+ },
+ },
+ "transform-enabled-disabled": {
+ severity: "warning",
+ messages: {
+ default: paramMessage`Transform "${"transformName"}" has been enabled and disabled in the same transform set.`,
+ },
+ },
+ "transform-engine-error": {
+ severity: "error",
+ messages: {
+ default: paramMessage`Failed to create mutation engine for transform "${"transformId"}": ${"error"}`,
+ },
+ },
+
/**
* Formatter
*/
diff --git a/packages/compiler/src/core/options.ts b/packages/compiler/src/core/options.ts
index 6f5d88140f1..7f40683855d 100644
--- a/packages/compiler/src/core/options.ts
+++ b/packages/compiler/src/core/options.ts
@@ -1,5 +1,5 @@
import { EmitterOptions, TypeSpecConfig } from "../config/types.js";
-import { LinterRuleSet, ParseOptions } from "./types.js";
+import { LinterRuleSet, ParseOptions, TransformSet } from "./types.js";
export interface CompilerOptions {
miscOptions?: Record;
@@ -68,6 +68,9 @@ export interface CompilerOptions {
/** Ruleset to enable for linting. */
linterRuleSet?: LinterRuleSet;
+ /** Transform set to enable for transformation. */
+ transformSet?: TransformSet;
+
/** @internal */
readonly configFile?: TypeSpecConfig;
}
diff --git a/packages/compiler/src/core/program.ts b/packages/compiler/src/core/program.ts
index c0eeefb81fd..6db05776035 100644
--- a/packages/compiler/src/core/program.ts
+++ b/packages/compiler/src/core/program.ts
@@ -43,6 +43,12 @@ import {
} from "./source-loader.js";
import { createStateAccessors } from "./state-accessors.js";
import { ComplexityStats, RuntimeStats, Stats, startTimer, time, timeAsync } from "./stats.js";
+import {
+ builtInTransformerLibraryName,
+ createBuiltInTransformerLibrary,
+ createTransformer,
+ resolveTransformerDefinition,
+} from "./transformer.js";
import {
CompilerHost,
Diagnostic,
@@ -52,8 +58,8 @@ import {
EmitterFunc,
Entity,
JsSourceFileNode,
- LibraryInstance,
LibraryMetadata,
+ LinterLibraryInstance,
LiteralType,
LocationContext,
LogSink,
@@ -68,6 +74,7 @@ import {
SyntaxKind,
TemplateInstanceTarget,
Tracer,
+ TransformerLibraryInstance,
Type,
TypeSpecLibrary,
TypeSpecScriptNode,
@@ -136,13 +143,20 @@ export interface Program {
readonly projectRoot: string;
}
+export interface TransformedProgram extends Program {
+ /**
+ * Result from running transformers, including mutation engines for accessing transformed types.
+ */
+ readonly transformerResult?: import("./transformer.js").TransformerResult;
+}
+
interface EmitterRef {
emitFunction: EmitterFunc;
main: string;
metadata: LibraryMetadata;
emitterOutputDir: string;
options: Record;
- readonly library: LibraryInstance;
+ readonly library: LinterLibraryInstance & TransformerLibraryInstance;
}
interface Validator {
@@ -207,7 +221,7 @@ async function createProgram(
mainFile: string,
options: CompilerOptions = {},
oldProgram?: Program,
-): Promise<{ program: Program; shouldAbort: boolean }> {
+): Promise<{ program: TransformedProgram; shouldAbort: boolean }> {
const runtimeStats: Partial = {};
const validateCbs: Validator[] = [];
const stateMaps = new Map>();
@@ -303,6 +317,16 @@ async function createProgram(
program.reportDiagnostics(await linter.extendRuleSet(options.linterRuleSet));
}
+ const transformer = createTransformer(program, (name) => loadLibrary(basedir, name));
+ // Register built-in transformer library (currently empty placeholder)
+ transformer.registerTransformLibrary(
+ builtInTransformerLibraryName,
+ createBuiltInTransformerLibrary(),
+ );
+ if (options.transformSet) {
+ program.reportDiagnostics(await transformer.extendTransformSet(options.transformSet));
+ }
+
program.checker = createChecker(program, resolver);
runtimeStats.checker = time(() => program.checker.checkProgram());
@@ -329,7 +353,17 @@ async function createProgram(
runtimeStats.linter = lintResult.stats.runtime;
program.reportDiagnostics(lintResult.diagnostics);
- return { program, shouldAbort: false };
+ // Transform stage
+ const transformResult = transformer.transform();
+ runtimeStats.transformer = transformResult.stats.runtime;
+ program.reportDiagnostics(transformResult.diagnostics);
+
+ // Attach transformer result to the program so consumers can access mutation engines
+ const transformedProgram: TransformedProgram = Object.assign(transformResult.program, {
+ transformerResult: transformResult,
+ });
+
+ return { program: transformedProgram, shouldAbort: false };
/**
* Validate the libraries loaded during the compilation process are compatible.
@@ -507,7 +541,7 @@ async function createProgram(
async function loadLibrary(
basedir: string,
libraryNameOrPath: string,
- ): Promise {
+ ): Promise<(LinterLibraryInstance & TransformerLibraryInstance) | undefined> {
const [resolution, diagnostics] = await resolveEmitterModuleAndEntrypoint(
basedir,
libraryNameOrPath,
@@ -522,11 +556,14 @@ async function createProgram(
const libDefinition: TypeSpecLibrary | undefined = entrypoint?.esmExports.$lib;
const metadata = computeLibraryMetadata(module, libDefinition);
const linterDef = entrypoint?.esmExports.$linter;
+ const transformerDef = entrypoint?.esmExports.$transformer;
return {
...resolution,
metadata,
definition: libDefinition,
linter: linterDef && resolveLinterDefinition(libraryNameOrPath, linterDef),
+ transformer:
+ transformerDef && resolveTransformerDefinition(libraryNameOrPath, transformerDef),
};
}
diff --git a/packages/compiler/src/core/stats.ts b/packages/compiler/src/core/stats.ts
index 936339ad103..cc00b059457 100644
--- a/packages/compiler/src/core/stats.ts
+++ b/packages/compiler/src/core/stats.ts
@@ -25,6 +25,13 @@ export interface RuntimeStats {
[rule: string]: number;
};
};
+ transformer: {
+ enabledTransforms: readonly string[];
+ total: number;
+ engineCreation: {
+ [transformId: string]: number;
+ };
+ };
emit: {
total: number;
emitters: {
diff --git a/packages/compiler/src/core/transformer.ts b/packages/compiler/src/core/transformer.ts
new file mode 100644
index 00000000000..c9b5154da81
--- /dev/null
+++ b/packages/compiler/src/core/transformer.ts
@@ -0,0 +1,307 @@
+import type { MutationEngine } from "@typespec/mutator-framework";
+import { compilerAssert, createDiagnosticCollector } from "./diagnostics.js";
+import { createDiagnostic } from "./messages.js";
+import type { Program, TransformedProgram } from "./program.js";
+import { startTimer } from "./stats.js";
+import {
+ Diagnostic,
+ NoTarget,
+ Transform,
+ TransformSet,
+ TransformSetRef,
+ TransformerDefinition,
+ TransformerResolvedDefinition,
+} from "./types.js";
+
+/**
+ * Minimal interface for transformer library data needed internally.
+ * The full TransformerLibraryInstance from types.ts includes module metadata
+ * that isn't needed for transformer registration.
+ */
+interface TransformerLibrary {
+ transformer: TransformerResolvedDefinition;
+}
+
+export interface Transformer {
+ /**
+ * Extend the current transform set with additional transforms.
+ * @param transformSet - The transform set configuration to apply
+ * @returns Diagnostics from processing the transform set
+ */
+ extendTransformSet(transformSet: TransformSet): Promise;
+
+ /**
+ * Register a transformer library.
+ * @param name - The library name
+ * @param lib - Optional library instance (will be loaded if not provided)
+ */
+ registerTransformLibrary(name: string, lib?: TransformerLibrary): Promise;
+
+ /**
+ * Execute all enabled transforms and create mutation engines.
+ * @returns The transformation result including diagnostics, program, and engines
+ */
+ transform(): TransformerResult;
+
+ /**
+ * Get the mutation engine for a specific transform.
+ * Useful for debugging and inspecting transform state.
+ * @param transformId - The fully qualified transform ID (e.g., "@library/transform-name")
+ * @returns The mutation engine if it exists, undefined otherwise
+ */
+ getEngine(transformId: string): MutationEngine | undefined;
+}
+
+export interface TransformerStats {
+ runtime: {
+ /** List of transform IDs that were enabled */
+ enabledTransforms: readonly string[];
+ /** Total time for all transform operations in milliseconds */
+ total: number;
+ /** Time spent creating each engine, keyed by transform ID */
+ engineCreation: Record;
+ };
+}
+export interface TransformerResult {
+ readonly diagnostics: readonly Diagnostic[];
+ readonly program: TransformedProgram;
+ readonly stats: TransformerStats;
+ readonly engines: ReadonlyMap>;
+}
+
+/** Resolve a transformer definition for a library. */
+export function resolveTransformerDefinition(
+ libName: string,
+ transformer: TransformerDefinition,
+): TransformerResolvedDefinition {
+ const transforms: Transform[] = transformer.transforms.map((t) => {
+ return { ...t, id: `${libName}/${t.name}` };
+ });
+ if (
+ transformer.transforms.length === 0 ||
+ (transformer.transformSets && "all" in transformer.transformSets)
+ ) {
+ return {
+ transforms,
+ transformSets: transformer.transformSets ?? {},
+ };
+ } else {
+ // Auto-generate an 'all' transform set that enables all transforms
+ const allEnable: Record = {};
+ for (const t of transforms) {
+ allEnable[t.id as TransformSetRef] = true;
+ }
+ return {
+ transforms,
+ transformSets: {
+ all: { enable: allEnable },
+ ...transformer.transformSets,
+ },
+ };
+ }
+}
+
+export function createTransformer(
+ program: Program,
+ loadLibrary: (name: string) => Promise,
+): Transformer {
+ const tracer = program.tracer.sub("transformer");
+
+ const transformMap = new Map>();
+ const enabledTransforms = new Map>();
+ const transformerLibraries = new Map();
+ const engines = new Map>();
+
+ return {
+ extendTransformSet,
+ registerTransformLibrary: async (name: string, lib?: TransformerLibrary) => {
+ await registerTransformLibraryInternal(name, lib);
+ },
+ transform,
+ getEngine: (transformId: string) => engines.get(transformId),
+ };
+
+ async function extendTransformSet(transformSet: TransformSet): Promise {
+ tracer.trace("extend-transform-set.start", JSON.stringify(transformSet, null, 2));
+ const diagnostics = createDiagnosticCollector();
+ if (transformSet.extends) {
+ for (const extendingTransformSetName of transformSet.extends) {
+ const ref = diagnostics.pipe(parseTransformReference(extendingTransformSetName));
+ if (ref) {
+ const library = await resolveLibrary(ref.libraryName);
+ const libTransformerDefinition = library?.transformer;
+ const extendingTransformSet = libTransformerDefinition?.transformSets?.[ref.name];
+ if (extendingTransformSet) {
+ await extendTransformSet(extendingTransformSet);
+ } else {
+ diagnostics.add(
+ createDiagnostic({
+ code: "unknown-transform-set",
+ format: { libraryName: ref.libraryName, transformSetName: ref.name },
+ target: NoTarget,
+ }),
+ );
+ }
+ }
+ }
+ }
+
+ const enabledInThisSet = new Set();
+ if (transformSet.enable) {
+ for (const [transformName, enable] of Object.entries(transformSet.enable)) {
+ if (enable === false) {
+ continue;
+ }
+ const ref = diagnostics.pipe(parseTransformReference(transformName as TransformSetRef));
+ if (ref) {
+ await resolveLibrary(ref.libraryName);
+ const transform = transformMap.get(transformName);
+ if (transform) {
+ enabledInThisSet.add(transformName);
+ enabledTransforms.set(transformName, transform);
+ } else {
+ diagnostics.add(
+ createDiagnostic({
+ code: "unknown-transform",
+ format: { libraryName: ref.libraryName, transformName: ref.name },
+ target: NoTarget,
+ }),
+ );
+ }
+ }
+ }
+ }
+
+ if (transformSet.disable) {
+ for (const transformName of Object.keys(transformSet.disable)) {
+ if (enabledInThisSet.has(transformName)) {
+ diagnostics.add(
+ createDiagnostic({
+ code: "transform-enabled-disabled",
+ format: { transformName },
+ target: NoTarget,
+ }),
+ );
+ }
+ enabledTransforms.delete(transformName);
+ }
+ }
+ tracer.trace(
+ "extend-transform-set.end",
+ "Transforms enabled: \n" + [...enabledTransforms.keys()].map((x) => ` - ${x}`).join("\n"),
+ );
+
+ return diagnostics.diagnostics;
+ }
+
+ function transform(): TransformerResult {
+ const diagnostics = createDiagnosticCollector();
+ const enabledTransformIds = [...enabledTransforms.keys()];
+ const stats: TransformerStats = {
+ runtime: {
+ enabledTransforms: enabledTransformIds,
+ total: 0,
+ engineCreation: {},
+ },
+ };
+ tracer.trace(
+ "transform",
+ `Running transformer with following transforms:\n` +
+ enabledTransformIds.map((x) => ` - ${x}`).join("\n"),
+ );
+
+ const timer = startTimer();
+
+ // Create mutation engines for all enabled transforms
+ for (const [id, t] of enabledTransforms.entries()) {
+ const engineTimer = startTimer();
+ try {
+ tracer.trace("transform.create-engine", `Creating engine for ${id}`);
+ const engine = t.createEngine(program);
+ engines.set(id, engine);
+ stats.runtime.engineCreation[id] = engineTimer.end();
+ tracer.trace("transform.engine-created", `Created engine for ${id}`);
+ } catch (error) {
+ diagnostics.add(
+ createDiagnostic({
+ code: "transform-engine-error",
+ format: { transformId: id, error: String(error) },
+ target: NoTarget,
+ }),
+ );
+ stats.runtime.engineCreation[id] = engineTimer.end();
+ }
+ }
+
+ // Note: With the mutator-framework, mutations are lazy - they happen when types are accessed,
+ // not upfront. The engines are stored and will be used when transformed types are requested.
+
+ stats.runtime.total = timer.end();
+
+ return {
+ diagnostics: diagnostics.diagnostics,
+ program: program as TransformedProgram,
+ stats,
+ engines,
+ };
+ }
+
+ async function resolveLibrary(name: string): Promise {
+ const loadedLibrary = transformerLibraries.get(name);
+ if (loadedLibrary === undefined) {
+ return registerTransformLibraryInternal(name);
+ }
+ return loadedLibrary;
+ }
+
+ async function registerTransformLibraryInternal(
+ name: string,
+ lib?: TransformerLibrary,
+ ): Promise {
+ tracer.trace("register-library", name);
+
+ const library = lib ?? (await loadLibrary(name));
+ const transformer = library?.transformer;
+ if (transformer?.transforms) {
+ for (const t of transformer.transforms) {
+ tracer.trace(
+ "register-library.transform",
+ `Registering transform "${t.id}" for library "${name}".`,
+ );
+ if (transformMap.has(t.id)) {
+ compilerAssert(false, `Unexpected duplicate transform: "${t.id}"`);
+ } else {
+ transformMap.set(t.id, t);
+ }
+ }
+ }
+ transformerLibraries.set(name, library);
+
+ return library;
+ }
+
+ function parseTransformReference(
+ ref: TransformSetRef,
+ ): [{ libraryName: string; name: string } | undefined, readonly Diagnostic[]] {
+ const segments = ref.split("/");
+ const name = segments.pop();
+ const libraryName = segments.join("/");
+ if (!libraryName || !name) {
+ return [
+ undefined,
+ [createDiagnostic({ code: "invalid-transform-ref", format: { ref }, target: NoTarget })],
+ ];
+ }
+ return [{ libraryName, name }, []];
+ }
+}
+
+export const builtInTransformerLibraryName = `@typespec/compiler/transformers`;
+export function createBuiltInTransformerLibrary(): TransformerLibrary {
+ // No built-in transforms yet; provide an empty definition.
+ const empty: TransformerResolvedDefinition = {
+ transforms: [],
+ transformSets: {},
+ };
+ return { transformer: empty };
+}
diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts
index 7dd56a806a2..88a52b388ad 100644
--- a/packages/compiler/src/core/types.ts
+++ b/packages/compiler/src/core/types.ts
@@ -1,5 +1,6 @@
+import type { MutationEngine } from "@typespec/mutator-framework";
import type { JSONSchemaType as AjvJSONSchemaType } from "ajv";
-import type { ModuleResolutionResult } from "../module-resolver/index.js";
+import type { ModuleResolutionResult } from "../module-resolver/types.js";
import type { YamlPathTarget, YamlScript } from "../yaml/types.js";
import type { Numeric } from "./numeric.js";
import type { Program } from "./program.js";
@@ -1897,9 +1898,16 @@ export interface LibraryInstance {
entrypoint: JsSourceFileNode;
metadata: LibraryMetadata;
definition?: TypeSpecLibrary;
+}
+
+export interface LinterLibraryInstance extends LibraryInstance {
linter: LinterResolvedDefinition;
}
+export interface TransformerLibraryInstance extends LibraryInstance {
+ transformer: TransformerResolvedDefinition;
+}
+
export type LibraryMetadata = FileLibraryMetadata | ModuleLibraryMetadata;
interface LibraryMetadataBase {
@@ -2411,6 +2419,69 @@ export type LinterRuleDiagnosticReport<
M extends keyof T = "default",
> = LinterRuleDiagnosticReportWithoutTarget & { target: DiagnosticTarget | typeof NoTarget };
+export interface TransformerDefinition {
+ transforms: TransformDefinition[];
+ transformSets?: Record;
+}
+
+export interface TransformerResolvedDefinition {
+ readonly transforms: Transform[];
+ readonly transformSets: {
+ [name: string]: TransformSet;
+ };
+}
+
+export interface TransformDefinition {
+ /** Transform name (without the library name) */
+ name: N;
+ /** Short description of the transform */
+ description: string;
+ /** Specifies the URL at which the full documentation can be accessed. */
+ url?: string;
+ /**
+ * Factory function that creates a mutation engine for this transform.
+ *
+ * The mutation engine defines how types in the type graph should be transformed.
+ * It manages mutation nodes, subgraphs, and caching of transformed types.
+ *
+ * @param program - The TypeSpec program to transform
+ * @returns A mutation engine configured for this transform
+ *
+ * @example
+ * ```ts
+ * import { $ } from "@typespec/compiler/typekit";
+ * import { SimpleMutationEngine, ModelMutation } from "@typespec/mutator-framework";
+ *
+ * createEngine: (program) => {
+ * const tk = $(program);
+ * return new SimpleMutationEngine(tk, {
+ * Model: RenameModelMutation,
+ * });
+ * }
+ * ```
+ */
+ createEngine: (program: Program) => MutationEngine;
+}
+
+/** Resolved instance of a transform that will run. */
+export interface Transform extends TransformDefinition {
+ /** Expanded transform id in format `:` */
+ id: string;
+}
+
+/** Reference to a transform. In this format `:` */
+export type TransformSetRef = `${string}/${string}`;
+export interface TransformSet {
+ /** Other transformset this transformset extends */
+ extends?: TransformSetRef[];
+
+ /** Transforms to enable/configure */
+ enable?: Record;
+
+ /** Transforms to disable. A transform CANNOT be in enable and disable map. */
+ disable?: Record;
+}
+
export interface TypeSpecLibrary<
T extends { [code: string]: DiagnosticMessages },
E extends Record = Record,
diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts
index 55ae15c153d..4c9aee775b1 100644
--- a/packages/compiler/src/index.ts
+++ b/packages/compiler/src/index.ts
@@ -93,9 +93,11 @@ export {
} from "./core/intrinsic-type-state.js";
export {
createLinterRule as createRule,
+ createTransform,
createTypeSpecLibrary,
defineLinter,
definePackageFlags,
+ defineTransformer,
paramMessage,
setTypeSpecNamespace,
} from "./core/library.js";
@@ -359,12 +361,12 @@ export type {
IntrinsicScalarName,
IntrinsicType,
JSONSchemaType,
- LibraryInstance,
LibraryLocationContext,
LibraryMetadata,
LineAndCharacter,
LineComment,
LinterDefinition,
+ LinterLibraryInstance,
LinterResolvedDefinition,
LinterRule,
LinterRuleContext,
@@ -440,6 +442,9 @@ export type {
TextRange,
Tracer,
TracerOptions,
+ TransformDefinition,
+ TransformerDefinition,
+ TransformerLibraryInstance,
Tuple,
Type,
TypeInstantiationMap,
diff --git a/packages/compiler/src/init/prompts.ts b/packages/compiler/src/init/prompts.ts
index 17dd6da1098..1c3983fda1a 100644
--- a/packages/compiler/src/init/prompts.ts
+++ b/packages/compiler/src/init/prompts.ts
@@ -7,9 +7,10 @@ export function checkbox(config: Parameters
+ pc.gray(
+ ` (Press ${pc.cyan("space")} to select, ${pc.cyan("a")} to toggle all, ${pc.cyan("i")} to invert selection and ${pc.cyan("enter")} to proceed.)`,
+ ),
},
icon: {
unchecked: pc.cyan(" ◯"),
diff --git a/packages/compiler/src/testing/index.ts b/packages/compiler/src/testing/index.ts
index 80fbe8465c0..8dc10a6f01f 100644
--- a/packages/compiler/src/testing/index.ts
+++ b/packages/compiler/src/testing/index.ts
@@ -42,6 +42,7 @@ export type {
TestHostError,
Tester,
TesterInstance,
+ TransformerTesterInstance,
TypeSpecTestLibrary,
TypeSpecTestLibraryInit,
} from "./types.js";
diff --git a/packages/compiler/src/testing/tester.ts b/packages/compiler/src/testing/tester.ts
index 1d2aed77ea0..7795ae46b03 100644
--- a/packages/compiler/src/testing/tester.ts
+++ b/packages/compiler/src/testing/tester.ts
@@ -1,3 +1,4 @@
+import { MutationOptions } from "@typespec/mutator-framework";
import { realpath } from "fs/promises";
import { pathToFileURL } from "url";
import { compilerAssert } from "../core/diagnostics.js";
@@ -8,7 +9,14 @@ import { getIdentifierContext, getNodeAtPosition } from "../core/parser.js";
import { getRelativePathFromDirectory, joinPaths, resolvePath } from "../core/path-utils.js";
import { Program, compile as coreCompile } from "../core/program.js";
import { createSourceLoader } from "../core/source-loader.js";
-import { CompilerHost, Diagnostic, Entity, NoTarget, SourceFile } from "../core/types.js";
+import {
+ CompilerHost,
+ Diagnostic,
+ Entity,
+ NoTarget,
+ SourceFile,
+ TransformSet,
+} from "../core/types.js";
import { resolveModule } from "../module-resolver/module-resolver.js";
import { NodePackageResolver } from "../module-resolver/node-package-resolver.js";
import { ResolveModuleHost } from "../module-resolver/types.js";
@@ -19,7 +27,7 @@ import { createTestFileSystem } from "./fs.js";
import { GetMarkedEntities, Marker, TemplateWithMarkers } from "./marked-template.js";
import { StandardTestLibrary, addTestLib } from "./test-compiler-host.js";
import { resolveVirtualPath } from "./test-utils.js";
-import type {
+import {
EmitterTester,
EmitterTesterInstance,
MockFile,
@@ -31,6 +39,8 @@ import type {
Tester,
TesterBuilder,
TesterInstance,
+ TransformerTester,
+ TransformerTesterInstance,
} from "./types.js";
export interface TesterOptions {
@@ -163,6 +173,10 @@ interface EmitterTesterInternalParams extends TesterInternalParams {
emitter: string;
}
+interface TransformerTesterInternalParams extends TesterInternalParams {
+ transformSet: TransformSet;
+}
+
function createTesterBuilder<
const I extends TesterInternalParams,
const O extends TesterBuilder,
@@ -227,6 +241,7 @@ function createTesterInternal(params: TesterInternalParams): Tester {
...createTesterBuilder(params, createTesterInternal),
emit,
createInstance,
+ transformer,
};
function emit(emitter: string, options?: Record): EmitterTester {
@@ -245,6 +260,16 @@ function createTesterInternal(params: TesterInternalParams): Tester {
});
}
+ function transformer(
+ transformSet: TransformSet,
+ options?: Record,
+ ): TransformerTester {
+ return createTransformerTesterInternal({
+ ...params,
+ transformSet,
+ });
+ }
+
function createInstance(): Promise {
return createTesterInstance(params);
}
@@ -274,6 +299,22 @@ function createEmitterTesterInternal(
};
}
+function createTransformerTesterInternal(
+ params: TransformerTesterInternalParams,
+): TransformerTester {
+ return {
+ ...createCompilable(async (...args) => {
+ const instance = await createTransformerTesterInstance(params);
+ return instance.compileAndDiagnose(...args);
+ }),
+ ...createTesterBuilder(
+ params,
+ createTransformerTesterInternal,
+ ),
+ createInstance: () => createTransformerTesterInstance(params),
+ };
+}
+
async function createEmitterTesterInstance(
params: EmitterTesterInternalParams,
): Promise> {
@@ -323,6 +364,37 @@ async function createEmitterTesterInstance(
}
}
+async function createTransformerTesterInstance(
+ params: TransformerTesterInternalParams,
+): Promise {
+ const tester = await createTesterInstance(params);
+ return {
+ fs: tester.fs,
+ ...createCompilable(compileAndDiagnose),
+ get program() {
+ return tester.program;
+ },
+ };
+
+ async function compileAndDiagnose(
+ code: string | TemplateWithMarkers | Record>,
+ options?: TestCompileOptions,
+ ): Promise<[TestCompileResult, readonly Diagnostic[]]> {
+ if (options?.compilerOptions?.transformSet !== undefined) {
+ throw new Error("Cannot set transformSet in options.");
+ }
+ const resolvedOptions: TestCompileOptions = {
+ ...options,
+ compilerOptions: {
+ ...params.compilerOptions,
+ ...options?.compilerOptions,
+ transformSet: params.transformSet,
+ },
+ };
+ return tester.compileAndDiagnose(code, resolvedOptions);
+ }
+}
+
async function createTesterInstance(params: TesterInternalParams): Promise {
let savedProgram: Program | undefined;
const fs = (await params.fs()).clone();
@@ -411,19 +483,84 @@ async function createTesterInstance(params: TesterInternalParams): Promise(
+ transformId: string,
+ type: TType,
+ ): TType => {
+ const transformedProgram = program as import("../core/program.js").TransformedProgram;
+ const engine = transformedProgram.transformerResult?.engines.get(transformId);
+ if (!engine) {
+ throw new Error(
+ `Transform "${transformId}" not found. Available transforms: ${Array.from(transformedProgram.transformerResult?.engines.keys() ?? []).join(", ")}`,
+ );
+ }
+ // Get the default subgraph from the engine
+ const MutationOptionsClass = (engine as any).constructor.MutationOptions;
+ const options = MutationOptionsClass ? new MutationOptionsClass() : new MutationOptions();
+ const subgraph = (engine as any).getDefaultMutationSubgraph?.(options);
+ if (!subgraph) {
+ throw new Error(`Engine for transform "${transformId}" does not have a default subgraph`);
+ }
+ // Type cast needed because tests use source types but mutator-framework uses compiled types
+ return engine.getMutatedType(subgraph as any, type as any) as TType;
+ };
+
+ // If transforms are enabled, automatically return mutated versions of entities
+ const transformedProgram = program as import("../core/program.js").TransformedProgram;
+ const mutatedEntities =
+ transformedProgram.transformerResult && transformedProgram.transformerResult.engines.size > 0
+ ? applyAllTransforms(entities, transformedProgram)
+ : entities;
+
return [
{
program,
fs,
pos: Object.fromEntries(markerPositions.map((x) => [x.name, x])) as any,
+ getMutatedType,
...(typesCollected as GetMarkedEntities),
- ...entities,
+ ...mutatedEntities,
},
program.diagnostics,
];
}
}
+function applyAllTransforms(
+ entities: Record,
+ transformedProgram: import("../core/program.js").TransformedProgram,
+): Record {
+ if (!transformedProgram.transformerResult) return entities;
+
+ const mutatedEntities: Record = {};
+ const engines = Array.from(transformedProgram.transformerResult.engines.values());
+
+ for (const [name, entity] of Object.entries(entities)) {
+ // Apply all transformation engines in sequence
+ let mutated: any = entity;
+ if (entity.entityKind === "Type") {
+ for (const engine of engines) {
+ // Get the default mutation options
+ const MutationOptionsClass = (engine as any).constructor.MutationOptions;
+ const options = MutationOptionsClass ? new MutationOptionsClass() : new MutationOptions();
+
+ // Mutate the entity using the engine's mutate() method
+ // This triggers the Mutation class's mutate() method
+ // Type cast needed because tests use source types but mutator-framework uses compiled types
+ const mutation = (engine as any).mutate?.(mutated, options);
+ if (mutation) {
+ mutated = mutation.getMutatedType();
+ }
+ }
+ }
+ mutatedEntities[name] = mutated;
+ }
+
+ return mutatedEntities;
+}
+
function extractMarkedEntities(
program: Program,
markerPositions: PositionedMarkerInFile[],
diff --git a/packages/compiler/src/testing/types.ts b/packages/compiler/src/testing/types.ts
index b35b08ac683..72a3e572457 100644
--- a/packages/compiler/src/testing/types.ts
+++ b/packages/compiler/src/testing/types.ts
@@ -1,6 +1,7 @@
+import { TransformerConfig } from "../config/index.js";
import type { CompilerOptions } from "../core/options.js";
-import type { Program } from "../core/program.js";
-import type { CompilerHost, Diagnostic, Entity, Type } from "../core/types.js";
+import type { Program, TransformedProgram } from "../core/program.js";
+import { CompilerHost, Diagnostic, Entity, TransformSet, Type } from "../core/types.js";
import { PositionedMarker } from "./fourslash.js";
import { GetMarkedEntities, TemplateWithMarkers } from "./marked-template.js";
@@ -58,6 +59,22 @@ export type TestCompileResult> = T & {
/** Position of all markers */
readonly pos: Record;
+
+ /**
+ * Get a mutated version of a type from a specific transform.
+ * This is used with lazy transformations where mutations are applied on-demand.
+ *
+ * @param transformId - The full transform ID (e.g., "@typespec/graphql/rename-types")
+ * @param type - The source type to get the mutated version of
+ * @returns The mutated type
+ *
+ * @example
+ * ```ts
+ * const result = await tester.compile(t.code`model ${t.model("Foo")} {}`);
+ * const mutatedFoo = result.getMutatedType("@typespec/graphql/rename-types", result.Foo);
+ * ```
+ */
+ getMutatedType?(transformId: string, type: TType): TType;
} & Record;
export interface TestCompileOptions {
@@ -146,6 +163,11 @@ export interface Tester extends Testable, TesterBuilder {
* @param options - Options to pass to the emitter
*/
emit(emitter: string, options?: Record): EmitterTester;
+ /**
+ * Create a transformer tester
+ * @param options - Options to pass to the transformer
+ */
+ transformer(transformSet: TransformSet, options?: TransformerConfig): TransformerTester;
/** Create an instance of the tester */
createInstance(): Promise;
}
@@ -193,6 +215,12 @@ export interface EmitterTester
createInstance(): Promise>;
}
+/** Alternate version of the tester which runs the configured transformer */
+export interface TransformerTester extends Testable, TesterBuilder {
+ /** Create a mutable instance of the tester */
+ createInstance(): Promise;
+}
+
export interface TesterInstanceBase {
/** Program created. Only available after calling `compile`, `diagnose` or `compileAndDiagnose` */
get program(): Program;
@@ -211,6 +239,11 @@ export interface PositionedMarkerInFile extends PositionedMarker {
readonly filename: string;
}
+/** Instance of a transformer tester */
+export interface TransformerTesterInstance extends TesterInstance {
+ get program(): TransformedProgram;
+}
+
// #endregion
// #region Legacy Test host
diff --git a/packages/compiler/templates/scaffolding.json b/packages/compiler/templates/scaffolding.json
index 77f752854d4..21a62d71428 100644
--- a/packages/compiler/templates/scaffolding.json
+++ b/packages/compiler/templates/scaffolding.json
@@ -2,7 +2,7 @@
"rest": {
"title": "Generic REST API",
"description": "Create a project representing a generic REST API service.",
- "compilerVersion": "1.6.0",
+ "compilerVersion": "1.7.1",
"libraries": [
"@typespec/http",
"@typespec/rest",
@@ -70,7 +70,7 @@
"target": "library",
"title": "TypeSpec library",
"description": "Build your own TypeSpec library with custom types, decorators or linters.",
- "compilerVersion": "1.6.0",
+ "compilerVersion": "1.7.1",
"libraries": [],
"files": [
{
@@ -147,7 +147,7 @@
"target": "library",
"title": "TypeSpec emitter",
"description": "Create a new package that emits artifacts from TypeSpec.",
- "compilerVersion": "1.6.0",
+ "compilerVersion": "1.7.1",
"libraries": [],
"files": [
{
diff --git a/packages/compiler/test/core/linter.test.ts b/packages/compiler/test/core/linter.test.ts
index 36da211dedc..2e7a3b56df3 100644
--- a/packages/compiler/test/core/linter.test.ts
+++ b/packages/compiler/test/core/linter.test.ts
@@ -4,8 +4,8 @@ import { createLinterRule, createTypeSpecLibrary } from "../../src/core/library.
import { Linter, createLinter, resolveLinterDefinition } from "../../src/core/linter.js";
import {
type Interface,
- type LibraryInstance,
type LinterDefinition,
+ type LinterLibraryInstance,
type LinterRuleContext,
} from "../../src/index.js";
import {
@@ -107,7 +107,7 @@ describe("compiler: linter", () => {
}
}
- const library: LibraryInstance = {
+ const library: LinterLibraryInstance = {
entrypoint: {} as any,
metadata: { type: "module", name: "@typespec/test-linter" },
module: { type: "module", path: "", mainFile: "", manifest: { name: "", version: "" } },
diff --git a/packages/compiler/test/core/transformer.test.ts b/packages/compiler/test/core/transformer.test.ts
new file mode 100644
index 00000000000..c50b21e8329
--- /dev/null
+++ b/packages/compiler/test/core/transformer.test.ts
@@ -0,0 +1,430 @@
+import { describe, expect, it } from "vitest";
+
+import { ModelMutation, SimpleMutationEngine } from "@typespec/mutator-framework";
+import {
+ builtInTransformerLibraryName,
+ createBuiltInTransformerLibrary,
+ createTransformer,
+ resolveTransformerDefinition,
+ Transformer,
+} from "../../src/core/transformer.js";
+import type { TransformerDefinition, TransformerLibraryInstance } from "../../src/index.js";
+import { createTransform, createTypeSpecLibrary } from "../../src/index.js";
+import {
+ createTestHost,
+ expectDiagnosticEmpty,
+ expectDiagnostics,
+} from "../../src/testing/index.js";
+import { $ } from "../../src/typekit/index.js";
+
+// Note: `as any` casts are needed below because test files use source types from src/
+// while @typespec/mutator-framework uses compiled types from dist/. This is a common
+// TypeScript monorepo issue that doesn't affect runtime behavior.
+
+const noopTransform = createTransform({
+ name: "noop",
+ description: "No operation transform",
+ createEngine: (program) => {
+ const tk = $(program) as any;
+ return new SimpleMutationEngine(tk, {});
+ },
+});
+
+// A transform that actually renames models by adding a prefix
+class PrefixModelMutation extends ModelMutation {
+ mutate() {
+ this.mutateType((model) => {
+ model.name = `Prefixed${model.name}`;
+ });
+ super.mutate();
+ }
+}
+
+const prefixTransform = createTransform({
+ name: "prefix",
+ description: "Add prefix to model names",
+ createEngine: (program) => {
+ const tk = $(program) as any;
+ return new SimpleMutationEngine(tk, {
+ Model: PrefixModelMutation,
+ });
+ },
+});
+
+// A transform that throws during engine creation
+const failingTransform = createTransform({
+ name: "failing",
+ description: "Transform that fails during engine creation",
+ createEngine: (_program) => {
+ throw new Error("Intentional engine creation failure");
+ },
+});
+
+describe("compiler: transformer", () => {
+ async function createTestTransformer(
+ code: string | Record,
+ transformerDef: TransformerDefinition,
+ ): Promise {
+ const host = await createTestHost();
+ if (typeof code === "string") {
+ host.addTypeSpecFile("main.tsp", code);
+ } else {
+ for (const [name, content] of Object.entries(code)) {
+ host.addTypeSpecFile(name, content);
+ }
+ }
+
+ const library: TransformerLibraryInstance = {
+ entrypoint: {} as any,
+ metadata: { type: "module", name: "@typespec/test-transformer" },
+ module: { type: "module", path: "", mainFile: "", manifest: { name: "", version: "" } },
+ definition: createTypeSpecLibrary({
+ name: "@typespec/test-transformer",
+ diagnostics: {},
+ }),
+ transformer: resolveTransformerDefinition("@typespec/test-transformer", transformerDef),
+ };
+
+ await host.compile("main.tsp");
+
+ return createTransformer(host.program, (libName) =>
+ Promise.resolve(libName === "@typespec/test-transformer" ? library : undefined),
+ );
+ }
+
+ it("registering a transform doesn't enable it", async () => {
+ const transformer = await createTestTransformer(`model Foo {}`, {
+ transforms: [noopTransform],
+ });
+ expectDiagnosticEmpty(transformer.transform().diagnostics);
+ });
+
+ it("enabling a transform that doesn't exist emits a diagnostic", async () => {
+ const transformer = await createTestTransformer(`model Foo {}`, {
+ transforms: [noopTransform],
+ });
+ expectDiagnostics(
+ await transformer.extendTransformSet({
+ enable: { "@typespec/test-transformer/not-a-transform": true },
+ }),
+ {
+ severity: "warning",
+ code: "unknown-transform",
+ message: /not-a-transform.*@typespec\/test-transformer/,
+ },
+ );
+ });
+
+ it("enabling a transform set that doesn't exist emits a diagnostic", async () => {
+ const transformer = await createTestTransformer(`model Foo {}`, {
+ transforms: [noopTransform],
+ });
+ expectDiagnostics(
+ await transformer.extendTransformSet({ extends: ["@typespec/test-transformer/not-a-set"] }),
+ {
+ severity: "warning",
+ code: "unknown-transform-set",
+ message: /not-a-set.*@typespec\/test-transformer/,
+ },
+ );
+ });
+
+ it("emits a diagnostic if enabling and disabling the same transform", async () => {
+ const transformer = await createTestTransformer(`model Foo {}`, {
+ transforms: [noopTransform],
+ });
+ expectDiagnostics(
+ await transformer.extendTransformSet({
+ enable: { "@typespec/test-transformer/noop": true },
+ disable: { "@typespec/test-transformer/noop": "Reason" },
+ }),
+ {
+ severity: "warning",
+ code: "transform-enabled-disabled",
+ message: /@typespec\/test-transformer\/noop.*enabled.*disabled/,
+ },
+ );
+ });
+
+ describe("when enabling a transform set", () => {
+ it("/all set is automatically provided and include all transforms", async () => {
+ const transformer = await createTestTransformer(`model Foo {}`, {
+ transforms: [noopTransform],
+ });
+ expectDiagnosticEmpty(
+ await transformer.extendTransformSet({
+ extends: ["@typespec/test-transformer/all"],
+ }),
+ );
+ // No diagnostics expected from running transforms as noop transform doesn't report any.
+ expectDiagnosticEmpty(transformer.transform().diagnostics);
+ });
+
+ it("extending specific transform set enables the transforms inside", async () => {
+ const transformer = await createTestTransformer(`model Foo {}`, {
+ transforms: [noopTransform],
+ transformSets: {
+ custom: {
+ enable: { "@typespec/test-transformer/noop": true },
+ },
+ },
+ });
+ expectDiagnosticEmpty(
+ await transformer.extendTransformSet({
+ extends: ["@typespec/test-transformer/custom"],
+ }),
+ );
+ expectDiagnosticEmpty(transformer.transform().diagnostics);
+ });
+
+ it("nested extends enables transforms from both sets", async () => {
+ const transformer = await createTestTransformer(`model Foo {}`, {
+ transforms: [noopTransform, prefixTransform],
+ transformSets: {
+ base: {
+ enable: { "@typespec/test-transformer/noop": true },
+ },
+ extended: {
+ extends: ["@typespec/test-transformer/base"],
+ enable: { "@typespec/test-transformer/prefix": true },
+ },
+ },
+ });
+ expectDiagnosticEmpty(
+ await transformer.extendTransformSet({
+ extends: ["@typespec/test-transformer/extended"],
+ }),
+ );
+ const result = transformer.transform();
+ expectDiagnosticEmpty(result.diagnostics);
+ // Both engines should be created
+ expect(result.engines.size).toBe(2);
+ });
+ });
+
+ describe("disable functionality", () => {
+ it("disabling a transform from an extended set removes it", async () => {
+ const transformer = await createTestTransformer(`model Foo {}`, {
+ transforms: [noopTransform, prefixTransform],
+ transformSets: {
+ base: {
+ enable: {
+ "@typespec/test-transformer/noop": true,
+ "@typespec/test-transformer/prefix": true,
+ },
+ },
+ },
+ });
+ expectDiagnosticEmpty(
+ await transformer.extendTransformSet({
+ extends: ["@typespec/test-transformer/base"],
+ disable: { "@typespec/test-transformer/noop": "Not needed" },
+ }),
+ );
+ const result = transformer.transform();
+ expectDiagnosticEmpty(result.diagnostics);
+ // Only prefix transform should be enabled
+ expect(result.engines.size).toBe(1);
+ expect(transformer.getEngine("@typespec/test-transformer/prefix")).toBeDefined();
+ expect(transformer.getEngine("@typespec/test-transformer/noop")).toBeUndefined();
+ });
+
+ it("setting enable to false does not enable the transform", async () => {
+ const transformer = await createTestTransformer(`model Foo {}`, {
+ transforms: [noopTransform],
+ });
+ expectDiagnosticEmpty(
+ await transformer.extendTransformSet({
+ enable: { "@typespec/test-transformer/noop": false },
+ }),
+ );
+ const result = transformer.transform();
+ expectDiagnosticEmpty(result.diagnostics);
+ expect(result.engines.size).toBe(0);
+ });
+ });
+
+ describe("getEngine", () => {
+ it("returns undefined for non-existent transform", async () => {
+ const transformer = await createTestTransformer(`model Foo {}`, {
+ transforms: [noopTransform],
+ });
+ expect(transformer.getEngine("@typespec/test-transformer/noop")).toBeUndefined();
+ });
+
+ it("returns the engine after transform is enabled and run", async () => {
+ const transformer = await createTestTransformer(`model Foo {}`, {
+ transforms: [noopTransform],
+ });
+ await transformer.extendTransformSet({
+ enable: { "@typespec/test-transformer/noop": true },
+ });
+ transformer.transform();
+ expect(transformer.getEngine("@typespec/test-transformer/noop")).toBeDefined();
+ });
+ });
+
+ describe("transform execution", () => {
+ it("creates engines for enabled transforms", async () => {
+ const transformer = await createTestTransformer(`model Foo {}`, {
+ transforms: [noopTransform],
+ });
+ await transformer.extendTransformSet({
+ enable: { "@typespec/test-transformer/noop": true },
+ });
+ const result = transformer.transform();
+ expectDiagnosticEmpty(result.diagnostics);
+ expect(result.engines.size).toBe(1);
+ expect(result.engines.has("@typespec/test-transformer/noop")).toBe(true);
+ });
+
+ it("tracks engine creation time in stats", async () => {
+ const transformer = await createTestTransformer(`model Foo {}`, {
+ transforms: [noopTransform],
+ });
+ await transformer.extendTransformSet({
+ enable: { "@typespec/test-transformer/noop": true },
+ });
+ const result = transformer.transform();
+ expect(result.stats.runtime.engineCreation["@typespec/test-transformer/noop"]).toBeDefined();
+ expect(result.stats.runtime.total).toBeGreaterThanOrEqual(0);
+ });
+
+ it("includes enabled transforms in stats", async () => {
+ const transformer = await createTestTransformer(`model Foo {}`, {
+ transforms: [noopTransform, prefixTransform],
+ });
+ await transformer.extendTransformSet({
+ enable: {
+ "@typespec/test-transformer/noop": true,
+ "@typespec/test-transformer/prefix": true,
+ },
+ });
+ const result = transformer.transform();
+ expect(result.stats.runtime.enabledTransforms).toContain("@typespec/test-transformer/noop");
+ expect(result.stats.runtime.enabledTransforms).toContain("@typespec/test-transformer/prefix");
+ expect(result.stats.runtime.enabledTransforms).toHaveLength(2);
+ });
+
+ it("runs multiple transforms in sequence", async () => {
+ const transformer = await createTestTransformer(`model Foo {}`, {
+ transforms: [noopTransform, prefixTransform],
+ });
+ await transformer.extendTransformSet({
+ enable: {
+ "@typespec/test-transformer/noop": true,
+ "@typespec/test-transformer/prefix": true,
+ },
+ });
+ const result = transformer.transform();
+ expectDiagnosticEmpty(result.diagnostics);
+ expect(result.engines.size).toBe(2);
+ });
+
+ it("emits diagnostic when engine creation fails", async () => {
+ const transformer = await createTestTransformer(`model Foo {}`, {
+ transforms: [failingTransform],
+ });
+ await transformer.extendTransformSet({
+ enable: { "@typespec/test-transformer/failing": true },
+ });
+ const result = transformer.transform();
+ expectDiagnostics(result.diagnostics, {
+ code: "transform-engine-error",
+ severity: "error",
+ message: /Failed to create mutation engine.*Intentional engine creation failure/,
+ });
+ });
+ });
+
+ describe("transform that mutates types", () => {
+ it("creates engine that can mutate model names", async () => {
+ const transformer = await createTestTransformer(`model Foo { x: string; }`, {
+ transforms: [prefixTransform],
+ });
+ await transformer.extendTransformSet({
+ enable: { "@typespec/test-transformer/prefix": true },
+ });
+ const result = transformer.transform();
+ expectDiagnosticEmpty(result.diagnostics);
+
+ const engine = result.engines.get("@typespec/test-transformer/prefix");
+ expect(engine).toBeDefined();
+
+ // Get the Foo model from the program
+ const Foo = result.program.getGlobalNamespaceType().models.get("Foo");
+ expect(Foo).toBeDefined();
+
+ // Use the engine to get the mutated type
+ const mutation = engine!.mutate(Foo! as any);
+ const mutatedFoo = mutation.getMutatedType();
+ expect(mutatedFoo.name).toBe("PrefixedFoo");
+ });
+ });
+});
+
+describe("resolveTransformerDefinition", () => {
+ it("adds library prefix to transform ids", () => {
+ const resolved = resolveTransformerDefinition("@my/lib", {
+ transforms: [noopTransform],
+ });
+ expect(resolved.transforms[0].id).toBe("@my/lib/noop");
+ });
+
+ it("auto-generates 'all' transform set when not provided", () => {
+ const resolved = resolveTransformerDefinition("@my/lib", {
+ transforms: [noopTransform],
+ });
+ expect(resolved.transformSets.all).toBeDefined();
+ expect(resolved.transformSets.all.enable).toEqual({
+ "@my/lib/noop": true,
+ });
+ });
+
+ it("does not override existing 'all' transform set", () => {
+ const resolved = resolveTransformerDefinition("@my/lib", {
+ transforms: [noopTransform],
+ transformSets: {
+ all: {
+ enable: { "@my/lib/custom": true },
+ },
+ },
+ });
+ expect(resolved.transformSets.all.enable).toEqual({
+ "@my/lib/custom": true,
+ });
+ });
+
+ it("preserves custom transform sets", () => {
+ const resolved = resolveTransformerDefinition("@my/lib", {
+ transforms: [noopTransform],
+ transformSets: {
+ custom: {
+ enable: { "@my/lib/noop": true },
+ },
+ },
+ });
+ expect(resolved.transformSets.custom).toBeDefined();
+ expect(resolved.transformSets.all).toBeDefined();
+ });
+
+ it("handles empty transforms array", () => {
+ const resolved = resolveTransformerDefinition("@my/lib", {
+ transforms: [],
+ });
+ expect(resolved.transforms).toHaveLength(0);
+ expect(resolved.transformSets).toEqual({});
+ });
+});
+
+describe("createBuiltInTransformerLibrary", () => {
+ it("returns an empty transformer definition", () => {
+ const lib = createBuiltInTransformerLibrary();
+ expect(lib.transformer.transforms).toHaveLength(0);
+ expect(lib.transformer.transformSets).toEqual({});
+ });
+
+ it("has the correct library name", () => {
+ expect(builtInTransformerLibraryName).toBe("@typespec/compiler/transformers");
+ });
+});
diff --git a/packages/compiler/test/testing/tester.test.ts b/packages/compiler/test/testing/tester.test.ts
index 819f98623db..33df821c8a8 100644
--- a/packages/compiler/test/testing/tester.test.ts
+++ b/packages/compiler/test/testing/tester.test.ts
@@ -1,9 +1,12 @@
// TODO: rename?
+import { SimpleMutationEngine } from "@typespec/mutator-framework";
import { strictEqual } from "assert";
import { describe, expect, expectTypeOf, it } from "vitest";
import { resolvePath } from "../../src/core/path-utils.js";
import {
+ createTransform,
+ defineTransformer,
EmitContext,
emitFile,
Enum,
@@ -17,6 +20,7 @@ import { mockFile } from "../../src/testing/fs.js";
import { t } from "../../src/testing/marked-template.js";
import { resolveVirtualPath } from "../../src/testing/test-utils.js";
import { createTester } from "../../src/testing/tester.js";
+import { $ } from "../../src/typekit/index.js";
const Tester = createTester(resolvePath(import.meta.dirname, "../.."), { libraries: [] });
@@ -357,3 +361,41 @@ describe("emitter", () => {
});
});
});
+
+describe("transformer", () => {
+ const TransformerTester = Tester.files({
+ "node_modules/dummy-transformer/package.json": JSON.stringify({
+ name: "dummy-transformer",
+ version: "1.0.0",
+ exports: { ".": "./index.js" },
+ }),
+ "node_modules/dummy-transformer/index.js": mockFile.js({
+ $transformer: defineTransformer({
+ transforms: [
+ createTransform({
+ name: "dummy-transform",
+ description: "A dummy transform.",
+ createEngine: (program) => {
+ const tk = $(program);
+ return new SimpleMutationEngine(tk, {});
+ },
+ }),
+ ],
+ }),
+ }),
+ }).transformer({ extends: ["dummy-transformer/all"] });
+
+ it("can transform", async () => {
+ const result = await TransformerTester.compile(t.code`
+ model ${t.model("Foo")} {}
+ `);
+ expect(result.Foo.kind).toBe("Model");
+ });
+
+ it("can wrap", async () => {
+ const result = await TransformerTester.wrap(
+ (x) => `model Test {}\n${x}\nmodel Test2 {}`,
+ ).compile(t.code`model ${t.model("Foo")} {}`);
+ expect(result.Foo.kind).toBe("Model");
+ });
+});
diff --git a/packages/emitter-framework/CHANGELOG.md b/packages/emitter-framework/CHANGELOG.md
index 0743748ac43..63d99036145 100644
--- a/packages/emitter-framework/CHANGELOG.md
+++ b/packages/emitter-framework/CHANGELOG.md
@@ -1,5 +1,9 @@
# Changelog - @typespec/emitter-framework
+## 0.14.0
+
+No changes, version bump only.
+
## 0.13.0
### Features
diff --git a/packages/emitter-framework/package.json b/packages/emitter-framework/package.json
index 83311704045..3dc777012e5 100644
--- a/packages/emitter-framework/package.json
+++ b/packages/emitter-framework/package.json
@@ -1,6 +1,6 @@
{
"name": "@typespec/emitter-framework",
- "version": "0.13.0",
+ "version": "0.14.0",
"type": "module",
"main": "dist/index.js",
"repository": {
@@ -55,16 +55,16 @@
"license": "MIT",
"description": "",
"peerDependencies": {
- "@alloy-js/core": "^0.21.0",
- "@alloy-js/csharp": "^0.21.0",
- "@alloy-js/typescript": "^0.21.0",
+ "@alloy-js/core": "^0.22.0",
+ "@alloy-js/csharp": "^0.22.0",
+ "@alloy-js/typescript": "^0.22.0",
"@typespec/compiler": "workspace:^"
},
"devDependencies": {
- "@alloy-js/cli": "^0.21.0",
- "@alloy-js/core": "^0.21.0",
+ "@alloy-js/cli": "^0.22.0",
+ "@alloy-js/core": "^0.22.0",
"@alloy-js/rollup-plugin": "^0.1.0",
- "@alloy-js/typescript": "^0.21.0",
+ "@alloy-js/typescript": "^0.22.0",
"@typespec/compiler": "workspace:^",
"concurrently": "^9.1.2",
"pathe": "^2.0.3",
diff --git a/packages/emitter-framework/src/core/index.ts b/packages/emitter-framework/src/core/index.ts
index 2b854ed8c66..6b3d6fb8a5a 100644
--- a/packages/emitter-framework/src/core/index.ts
+++ b/packages/emitter-framework/src/core/index.ts
@@ -1,4 +1,6 @@
export * from "./components/index.js";
export * from "./context/index.js";
+export * from "./scc-set.js";
export * from "./transport-name-policy.js";
+export * from "./type-connector.js";
export * from "./write-output.js";
diff --git a/packages/emitter-framework/src/core/scc-set.test.ts b/packages/emitter-framework/src/core/scc-set.test.ts
new file mode 100644
index 00000000000..19dbc746eba
--- /dev/null
+++ b/packages/emitter-framework/src/core/scc-set.test.ts
@@ -0,0 +1,293 @@
+import { Tester } from "#test/test-host.js";
+import { computed } from "@alloy-js/core";
+import type { Type } from "@typespec/compiler";
+import { t } from "@typespec/compiler/testing";
+import { describe, expect, it, vi } from "vitest";
+import { SCCSet, type NestedArray, type SCCComponent } from "./scc-set.js";
+import { typeDependencyConnector } from "./type-connector.js";
+
+describe("SCCSet", () => {
+ it("topologically orders items", () => {
+ const edges = new Map([
+ ["model", ["serializer"]],
+ ["serializer", ["helpers"]],
+ ["helpers", []],
+ ]);
+
+ const set = new SCCSet((item) => edges.get(item) ?? []);
+ set.add("model");
+ set.add("serializer");
+ set.add("helpers");
+
+ expect([...set.items]).toEqual(["helpers", "serializer", "model"]);
+ expect(componentValues(set)).toEqual(["helpers", "serializer", "model"]);
+ });
+
+ it("groups strongly connected components", () => {
+ const edges = new Map([
+ ["a", ["b"]],
+ ["b", ["a", "c"]],
+ ["c", []],
+ ]);
+
+ const set = new SCCSet((item) => edges.get(item) ?? []);
+ set.add("a");
+ set.add("b");
+ set.add("c");
+
+ expect(componentValues(set)).toEqual(["c", ["a", "b"]]);
+ expect(set.items).toEqual(["c", "a", "b"]);
+ });
+
+ it("defers placeholders until added", () => {
+ const edges = new Map([
+ ["root", ["child"]],
+ ["child", []],
+ ]);
+
+ const set = new SCCSet((item) => edges.get(item) ?? []);
+
+ set.add("root");
+ expect(set.items).toEqual(["root"]);
+ expect(componentValues(set)).toEqual(["root"]);
+
+ set.add("child");
+ expect(set.items).toEqual(["child", "root"]);
+ expect(componentValues(set)).toEqual(["child", "root"]);
+ });
+
+ it("surfaces reachable nodes when requested", () => {
+ const edges = new Map([
+ ["root", ["child"]],
+ ["child", ["leaf"]],
+ ["leaf", []],
+ ]);
+
+ const set = new SCCSet((item) => edges.get(item) ?? [], { includeReachable: true });
+ set.add("root");
+
+ expect(set.items).toEqual(["leaf", "child", "root"]);
+ expect(componentValues(set)).toEqual(["leaf", "child", "root"]);
+ });
+
+ it("mutates arrays in place when adding", () => {
+ const edges = new Map([["only", []]]);
+ const connector = vi.fn((item: string) => edges.get(item) ?? []);
+
+ const set = new SCCSet(connector);
+ set.add("only");
+
+ const firstItems = set.items;
+ const firstComponents = set.components;
+
+ expect(set.items).toBe(firstItems);
+ expect(set.components).toBe(firstComponents);
+ expect(connector).toHaveBeenCalledTimes(1);
+
+ set.add("late");
+ expect(connector).toHaveBeenCalledTimes(2);
+ expect(set.items).toBe(firstItems);
+ expect(set.components).toBe(firstComponents);
+ expect(firstItems).toEqual(["only", "late"]);
+ expect(componentValuesFrom(firstComponents)).toEqual(["only", "late"]);
+ });
+
+ it("notifies computed observers", () => {
+ const edges = new Map([
+ ["model", ["serializer"]],
+ ["serializer", ["helpers"]],
+ ["helpers", []],
+ ["cycle-a", ["cycle-b"]],
+ ["cycle-b", ["cycle-a"]],
+ ]);
+
+ const set = new SCCSet((item) => edges.get(item) ?? []);
+ const observedItems = computed(() => [...set.items]);
+ const observedComponents = computed(() => componentValues(set));
+ const observedCycle = computed(() => {
+ const cycle = set.components.find((component) => Array.isArray(component.value));
+ if (!cycle || !Array.isArray(cycle.value)) {
+ return [];
+ }
+ return [...cycle.value];
+ });
+
+ expect(observedItems.value).toEqual([]);
+ expect(observedComponents.value).toEqual([]);
+ expect(observedCycle.value).toEqual([]);
+
+ set.add("model");
+ expect(observedItems.value).toEqual(["model"]);
+ expect(observedComponents.value).toEqual(["model"]);
+
+ set.add("serializer");
+ expect(observedItems.value).toEqual(["serializer", "model"]);
+ expect(observedComponents.value).toEqual(["serializer", "model"]);
+
+ set.add("helpers");
+ expect(observedItems.value).toEqual(["helpers", "serializer", "model"]);
+ expect(observedComponents.value).toEqual(["helpers", "serializer", "model"]);
+
+ set.add("cycle-a");
+ expect(observedCycle.value).toEqual([]);
+
+ set.add("cycle-b");
+ expect(observedCycle.value).toEqual(["cycle-a", "cycle-b"]);
+ });
+
+ it("orders dependent nodes even when added out of order", () => {
+ const edges = new Map([
+ ["Leaf", []],
+ ["Indexed", ["Record"]],
+ ["Record", ["Leaf"]],
+ ["Base", ["Leaf"]],
+ ["Derived", ["Base", "Indexed"]],
+ ["CycleA", ["CycleB"]],
+ ["CycleB", ["CycleA"]],
+ ]);
+
+ const insertionOrder = ["Derived", "Indexed", "Leaf", "Base", "CycleA", "CycleB"];
+ const set = new SCCSet((item) => edges.get(item) ?? []);
+ for (const item of insertionOrder) {
+ set.add(item);
+ }
+
+ expect([...set.items]).toEqual(["Leaf", "Indexed", "Base", "Derived", "CycleA", "CycleB"]);
+ expect(componentValues(set)).toEqual([
+ "Leaf",
+ "Indexed",
+ "Base",
+ "Derived",
+ ["CycleA", "CycleB"],
+ ]);
+ });
+
+ it("batch adds nodes and recomputes once", () => {
+ const edges = new Map([
+ ["Leaf", []],
+ ["Indexed", ["Record"]],
+ ["Record", ["Leaf"]],
+ ["Base", ["Leaf"]],
+ ["Derived", ["Base", "Indexed"]],
+ ["CycleA", ["CycleB"]],
+ ["CycleB", ["CycleA"]],
+ ]);
+
+ const insertionOrder = ["Derived", "Indexed", "Leaf", "Base", "CycleA", "CycleB"];
+ const set = new SCCSet((item) => edges.get(item) ?? []);
+ set.addAll(insertionOrder);
+
+ expect([...set.items]).toEqual(["Leaf", "Indexed", "Base", "Derived", "CycleA", "CycleB"]);
+ expect(componentValues(set)).toEqual([
+ "Leaf",
+ "Indexed",
+ "Base",
+ "Derived",
+ ["CycleA", "CycleB"],
+ ]);
+ });
+
+ it("exposes component connections", () => {
+ const edges = new Map([
+ ["Leaf", []],
+ ["Base", ["Leaf"]],
+ ["Indexed", ["Leaf"]],
+ ["Derived", ["Base", "Indexed"]],
+ ["CycleA", ["CycleB"]],
+ ["CycleB", ["CycleA"]],
+ ]);
+
+ const set = new SCCSet((item) => edges.get(item) ?? []);
+ set.addAll(["Derived", "Base", "Indexed", "Leaf", "CycleA", "CycleB"]);
+
+ const getSingleton = (name: string) =>
+ set.components.find((component) => component.value === name)!;
+ const format = (components: Iterable>) =>
+ Array.from(components, (component) =>
+ Array.isArray(component.value) ? component.value.join(",") : component.value,
+ ).sort();
+
+ const derived = getSingleton("Derived");
+ const base = getSingleton("Base");
+ const indexed = getSingleton("Indexed");
+ const leaf = getSingleton("Leaf");
+ const cycle = set.components.find((component) => Array.isArray(component.value))!;
+
+ expect(format(derived.references)).toEqual(["Base", "Indexed"]);
+ expect(format(base.references)).toEqual(["Leaf"]);
+ expect(format(base.referencedBy)).toEqual(["Derived"]);
+ expect(format(indexed.referencedBy)).toEqual(["Derived"]);
+ expect(format(leaf.referencedBy)).toEqual(["Base", "Indexed"]);
+ expect(format(cycle.references)).toEqual([]);
+ expect(format(cycle.referencedBy)).toEqual([]);
+ });
+
+ it("orders TypeSpec models via connector", async () => {
+ const tester = await Tester.createInstance();
+ const { Leaf, Indexed, Base, Derived, CycleA, CycleB } = await tester.compile(
+ t.code`
+ @test model ${t.model("Leaf")} {
+ value: string;
+ }
+
+ @test model ${t.model("Indexed")} extends Record {}
+
+ @test model ${t.model("Base")} {
+ leaf: Leaf;
+ }
+
+ @test model ${t.model("Derived")} extends Base {
+ payload: Indexed;
+ }
+
+ @test model ${t.model("CycleA")} {
+ next: CycleB;
+ }
+
+ @test model ${t.model("CycleB")} {
+ prev: CycleA;
+ }
+ `,
+ );
+
+ const models = [Derived, Indexed, Leaf, Base, CycleA, CycleB] as Type[];
+ const set = new SCCSet(typeDependencyConnector);
+ for (const type of models) {
+ set.add(type);
+ }
+
+ const itemNames = set.items.map(getTypeLabel);
+ expect(itemNames).toEqual(["Leaf", "Indexed", "Base", "Derived", "CycleA", "CycleB"]);
+
+ const componentNames = set.components.map(formatComponent);
+ expect(componentNames).toEqual(["Leaf", "Indexed", "Base", "Derived", ["CycleA", "CycleB"]]);
+ });
+});
+
+function componentValues(set: SCCSet): NestedArray[] {
+ return componentValuesFrom(set.components);
+}
+
+function componentValuesFrom(components: readonly SCCComponent[]): NestedArray[] {
+ return components.map((component) => component.value);
+}
+
+function getTypeLabel(type: Type): string {
+ if ("name" in type && typeof type.name === "string" && type.name) {
+ return type.name;
+ }
+ return type.kind;
+}
+
+type ComponentLabel = string | string[];
+
+function formatComponent(component: SCCComponent): ComponentLabel {
+ return formatComponentValue(component.value);
+}
+
+function formatComponentValue(componentValue: NestedArray): ComponentLabel {
+ if (Array.isArray(componentValue)) {
+ return (componentValue as Type[]).map(getTypeLabel);
+ }
+ return getTypeLabel(componentValue);
+}
diff --git a/packages/emitter-framework/src/core/scc-set.ts b/packages/emitter-framework/src/core/scc-set.ts
new file mode 100644
index 00000000000..dafd907c1a0
--- /dev/null
+++ b/packages/emitter-framework/src/core/scc-set.ts
@@ -0,0 +1,777 @@
+import { shallowReactive } from "@alloy-js/core";
+
+export type NestedArray = T | NestedArray[];
+
+export interface SCCComponent {
+ /**
+ * Nested array representation of the items that belong to this component.
+ * Single-node components expose the item directly while cycles expose a nested array.
+ */
+ readonly value: NestedArray;
+ /** Components that this component depends on (outgoing edges). */
+ readonly references: ReadonlySet>;
+ /** Components that depend on this component (incoming edges). */
+ readonly referencedBy: ReadonlySet>;
+}
+
+type Connector = (item: T) => Iterable;
+
+export interface SCCSetOptions {
+ /**
+ * When true, every node reachable from an added node is automatically surfaced
+ * in the public `items`/`components` lists without requiring an explicit add.
+ */
+ includeReachable?: boolean;
+}
+
+interface ComponentRecord {
+ nodes: NodeRecord[];
+ value: NestedArray;
+ size: number;
+ view: ComponentView;
+}
+
+interface ComponentView extends SCCComponent {
+ readonly references: Set>;
+ readonly referencedBy: Set>;
+}
+
+interface NodeRecord {
+ readonly item: T;
+ readonly neighbors: Set>;
+ readonly dependents: Set>;
+ added: boolean;
+ addedAt?: number;
+ component?: ComponentRecord;
+ initialized: boolean;
+}
+
+interface RemovedComponent {
+ component: ComponentRecord;
+ index: number;
+}
+
+/**
+ * Maintains a growing directed graph and exposes its strongly connected components (SCCs).
+ *
+ * The set incrementally applies Tarjan's algorithm so newly added nodes immediately update
+ * the public `items` and `components` views. Both arrays are shallow reactive so observers
+ * can hold references without re-fetching after each mutation.
+ */
+export class SCCSet {
+ /**
+ * Flattened, topologically ordered view of every node that has been added to the set.
+ * Nodes appear before dependents unless they belong to the same strongly connected component.
+ */
+ public readonly items: T[];
+
+ /**
+ * Ordered strongly connected components that mirror `items`. Each entry exposes its members along
+ * with the components it depends on and the components that depend on it, enabling callers to walk
+ * the connectivity graph directly from any component.
+ */
+ public readonly components: SCCComponent[];
+
+ readonly #nodes = new Map>();
+ readonly #connector: Connector;
+ readonly #componentOrder: ComponentRecord[] = [];
+ #addCounter = 0;
+ readonly #includeReachable: boolean;
+
+ /**
+ * Creates a new SCC set around the provided dependency connector function.
+ * @param connector Maps each item to the items it depends on (outgoing edges).
+ * @param options Controls automatic inclusion of reachable nodes.
+ */
+ constructor(connector: Connector, options: SCCSetOptions = {}) {
+ this.#connector = connector;
+ this.items = shallowReactive([]);
+ this.components = shallowReactive[]>([]);
+ this.#includeReachable = !!options.includeReachable;
+ }
+
+ /**
+ * Adds an item to the graph and captures its outgoing connections via the connector.
+ * Items can be referenced before they are added; they will only surface in the public
+ * views once explicitly added.
+ */
+ public add(item: T): void {
+ const node = this.#getOrCreateNode(item);
+ if (node.added) {
+ return;
+ }
+
+ node.added = true;
+ node.addedAt = this.#addCounter++;
+ this.#initializeNode(node, true);
+
+ if (this.#includeReachable) {
+ this.#autoAddReachable(node);
+ this.#recomputeAll();
+ } else {
+ this.#integrateNode(node);
+ }
+ }
+
+ /**
+ * Adds multiple items and recomputes SCC ordering once at the end.
+ */
+ public addAll(items: Iterable): void {
+ const newlyAdded: NodeRecord[] = [];
+ for (const item of items) {
+ const node = this.#getOrCreateNode(item);
+ if (node.added) {
+ continue;
+ }
+ node.added = true;
+ node.addedAt = this.#addCounter++;
+ this.#initializeNode(node, true);
+ newlyAdded.push(node);
+ }
+
+ if (newlyAdded.length === 0) {
+ return;
+ }
+
+ if (this.#includeReachable) {
+ for (const node of newlyAdded) {
+ this.#autoAddReachable(node);
+ }
+ }
+
+ this.#recomputeAll();
+ }
+
+ /**
+ * Recursively adds every node reachable from the starting node, initializing metadata as needed.
+ */
+ #autoAddReachable(start: NodeRecord): void {
+ const visited = new Set>([start]);
+ const stack = [...start.neighbors];
+
+ while (stack.length > 0) {
+ const current = stack.pop()!;
+ if (visited.has(current)) {
+ continue;
+ }
+ visited.add(current);
+
+ if (!current.added) {
+ current.added = true;
+ current.addedAt = this.#addCounter++;
+ this.#initializeNode(current, true);
+ }
+
+ for (const neighbor of current.neighbors) {
+ if (!visited.has(neighbor)) {
+ stack.push(neighbor);
+ }
+ }
+ }
+ }
+
+ /**
+ * Retrieves the cached node for the provided item or materializes a new, uninitialized record.
+ */
+ #getOrCreateNode(item: T): NodeRecord {
+ let existing = this.#nodes.get(item);
+ if (!existing) {
+ existing = {
+ item,
+ neighbors: new Set>(),
+ dependents: new Set>(),
+ added: false,
+ initialized: false,
+ } satisfies NodeRecord;
+ this.#nodes.set(item, existing);
+ }
+ return existing;
+ }
+
+ /**
+ * Runs the connector for the node to refresh its neighbors and dependent relationships.
+ * @param force When true, existing neighbor edges are cleared before recomputing.
+ */
+ #initializeNode(node: NodeRecord, force = false): void {
+ if (!force && node.initialized) {
+ return;
+ }
+
+ if (force || node.initialized) {
+ for (const neighbor of node.neighbors) {
+ neighbor.dependents.delete(node);
+ }
+ node.neighbors.clear();
+ }
+
+ const dependencies = this.#connector(node.item);
+ node.initialized = true;
+ for (const dependency of dependencies) {
+ if (dependency === undefined) {
+ throw new Error(
+ `Connector returned undefined dependency while initializing ${String(node.item)}`,
+ );
+ }
+ const neighbor = this.#getOrCreateNode(dependency);
+ node.neighbors.add(neighbor);
+ neighbor.dependents.add(node);
+ if (!neighbor.added) {
+ this.#initializeNode(neighbor);
+ }
+ }
+ }
+
+ /**
+ * Inserts a node that was just added into the component ordering without recomputing the world.
+ */
+ #integrateNode(node: NodeRecord): void {
+ const forward = this.#collectReachable(node, (current) => current.neighbors);
+ const backward = this.#collectReachable(node, (current) => current.dependents);
+ const candidates = new Set>();
+ for (const seen of forward) {
+ if (backward.has(seen)) {
+ candidates.add(seen);
+ }
+ }
+ candidates.add(node);
+
+ const orderedNodes = Array.from(candidates).sort(
+ (left, right) => (left.addedAt ?? 0) - (right.addedAt ?? 0),
+ );
+
+ const dependencyComponents = this.#collectNeighboringComponents(
+ orderedNodes,
+ (nodeRecord) => nodeRecord.neighbors,
+ candidates,
+ );
+ const dependentComponents = this.#collectNeighboringComponents(
+ orderedNodes,
+ (nodeRecord) => nodeRecord.dependents,
+ candidates,
+ );
+
+ const dependentClosure = this.#collectDependentComponentClosure(orderedNodes, candidates);
+ const sortedDependents = this.#sortComponentsTopologically(dependentClosure);
+
+ const insertIndexBeforeRemoval = this.#computeInsertIndex(
+ dependencyComponents,
+ dependentComponents,
+ );
+ const componentsToRemove = new Set>();
+ for (const member of candidates) {
+ if (member.component) {
+ componentsToRemove.add(member.component);
+ }
+ }
+ for (const component of dependentClosure) {
+ componentsToRemove.add(component);
+ }
+
+ const removedComponents = this.#removeComponents(componentsToRemove);
+ const newComponent = this.#createComponent(orderedNodes);
+ const insertIndex = this.#adjustInsertIndex(insertIndexBeforeRemoval, removedComponents);
+ this.#insertComponent(newComponent, insertIndex);
+
+ let nextIndex = insertIndex + 1;
+ for (const component of sortedDependents) {
+ this.#insertComponent(component, nextIndex++);
+ }
+
+ this.#refreshComponentConnections();
+ }
+
+ /**
+ * Walks the graph in the provided direction to find all reachable, added nodes.
+ */
+ #collectReachable(
+ start: NodeRecord,
+ next: (node: NodeRecord) => Iterable>,
+ ): Set> {
+ const visited = new Set>();
+ const stack: NodeRecord[] = [start];
+ while (stack.length > 0) {
+ const current = stack.pop()!;
+ if (visited.has(current) || !current.added) {
+ continue;
+ }
+ visited.add(current);
+ for (const neighbor of next(current)) {
+ if (neighbor.added && !visited.has(neighbor)) {
+ stack.push(neighbor);
+ }
+ }
+ }
+ return visited;
+ }
+
+ /**
+ * Collects components adjacent to the provided nodes that are not part of an excluded set.
+ */
+ #collectNeighboringComponents(
+ nodes: NodeRecord[],
+ next: (node: NodeRecord) => Iterable>,
+ excluded: Set>,
+ ): Set> {
+ const components = new Set>();
+ for (const node of nodes) {
+ for (const neighbor of next(node)) {
+ if (!neighbor.added || excluded.has(neighbor) || !neighbor.component) {
+ continue;
+ }
+ components.add(neighbor.component);
+ }
+ }
+ return components;
+ }
+
+ /**
+ * Computes the closure of components that depend (directly or indirectly) on the start nodes.
+ */
+ #collectDependentComponentClosure(
+ startNodes: NodeRecord[],
+ excluded: Set>,
+ ): Set> {
+ const closure = new Set>();
+ const visited = new Set>();
+ const stack = [...startNodes];
+
+ while (stack.length > 0) {
+ const current = stack.pop()!;
+ if (visited.has(current)) {
+ continue;
+ }
+ visited.add(current);
+
+ for (const dependent of current.dependents) {
+ if (excluded.has(dependent) || visited.has(dependent)) {
+ continue;
+ }
+
+ if (dependent.added && dependent.component) {
+ if (!closure.has(dependent.component)) {
+ closure.add(dependent.component);
+ for (const member of dependent.component.nodes) {
+ stack.push(member);
+ }
+ }
+ } else {
+ stack.push(dependent);
+ }
+ }
+ }
+
+ return closure;
+ }
+
+ /**
+ * Sorts the provided components in topological order, falling back to insertion order on cycles.
+ */
+ #sortComponentsTopologically(components: Set>): ComponentRecord[] {
+ if (components.size === 0) {
+ return [];
+ }
+
+ const componentList = Array.from(components);
+ const inDegree = new Map, number>();
+ const adjacency = new Map, Set