-
Notifications
You must be signed in to change notification settings - Fork 350
Auto-generate ConfigurationSchema.json for JSON IntelliSense #10139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Copilot
wants to merge
19
commits into
main
Choose a base branch
from
copilot/add-auto-generate-configuration-schema-json
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+4,441
−3
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
a2aa0dd
Initial plan
Copilot 4025e20
feat: auto-generate ConfigurationSchema.json for JSON IntelliSense
Copilot a1ac5df
Address code review feedback: optimize enum lookup and add comments
Copilot da4dd67
Add changelog entry for ConfigurationSchema.json feature
Copilot 3541fb5
Remove Azure-specific logic from base emitter ConfigurationSchemaGene…
Copilot f3f72b7
Delete chronus changelog file per reviewer request
Copilot 2c0a31e
Add regenerated ConfigurationSchema.json files for test projects
Copilot 27184ed
Add definitions section for credential and options to ConfigurationSc…
Copilot be700ca
Update Check-GitChanges.ps1 to detect untracked files
Copilot 4195716
Remove local definitions from ConfigurationSchema.json
Copilot e83c81d
Add local definitions for non-base types (enums) in ConfigurationSche…
Copilot b0f6975
Fix edge case: handle single-character enum names in definition naming
Copilot 3de54bd
Model options type inheritance from core type via named local definit…
Copilot 06c5dba
Add test cases for options type with additional properties (string, i…
Copilot 1d975e4
Add descriptive assertion message for allOf count in integer property…
Copilot 3db3324
Add test for options property with model definition and support model…
Copilot 577eac7
Add test for model as constructor parameter in ConfigurationSchema
Copilot d0fd621
Address PR review feedback
JoshLove-msft 8b61ad8
Normalize line endings and add trailing newline in generated schema
JoshLove-msft File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
377 changes: 377 additions & 0 deletions
377
...rp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/ConfigurationSchemaGenerator.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,377 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using System.Text.Json; | ||
| using System.Text.Json.Nodes; | ||
| using Microsoft.TypeSpec.Generator.ClientModel.Providers; | ||
| using Microsoft.TypeSpec.Generator.Input.Extensions; | ||
| using Microsoft.TypeSpec.Generator.Primitives; | ||
| using Microsoft.TypeSpec.Generator.Providers; | ||
|
|
||
| namespace Microsoft.TypeSpec.Generator.ClientModel | ||
| { | ||
| /// <summary> | ||
| /// Generates a ConfigurationSchema.json file for JSON IntelliSense support in appsettings.json. | ||
| /// The schema defines well-known client names and their configuration properties. | ||
| /// Common definitions (credential, options) are inherited from the System.ClientModel base schema | ||
| /// and not duplicated here. Only additional types specific to the generated client (e.g., enums, | ||
| /// custom models) are defined locally. | ||
| /// </summary> | ||
| internal static class ConfigurationSchemaGenerator | ||
| { | ||
| internal const string DefaultSectionName = "Clients"; | ||
| internal const string DefaultOptionsRef = "options"; | ||
|
|
||
| private static readonly JsonSerializerOptions s_jsonOptions = new() | ||
| { | ||
| WriteIndented = true | ||
| }; | ||
|
|
||
| /// <summary> | ||
| /// Generates the ConfigurationSchema.json content based on the output library's type providers. | ||
| /// Returns null if no clients with <see cref="ClientSettingsProvider"/> are found. | ||
| /// </summary> | ||
| internal static string? Generate(OutputLibrary output, string sectionName = DefaultSectionName, string optionsRef = DefaultOptionsRef) | ||
| { | ||
| var clientsWithSettings = output.TypeProviders | ||
| .OfType<ClientProvider>() | ||
| .Where(c => c.ClientSettings != null) | ||
| .ToList(); | ||
|
|
||
| if (clientsWithSettings.Count == 0) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var schema = BuildSchema(clientsWithSettings, sectionName, optionsRef); | ||
| return JsonSerializer.Serialize(schema, s_jsonOptions).ReplaceLineEndings("\n") + "\n"; | ||
| } | ||
|
|
||
| private static JsonObject BuildSchema( | ||
| List<ClientProvider> clients, | ||
| string sectionName, | ||
| string optionsRef) | ||
| { | ||
| // Collect local definitions for non-base types during schema generation | ||
| var localDefinitions = new Dictionary<string, JsonObject>(); | ||
| var clientProperties = new JsonObject(); | ||
|
|
||
| foreach (var client in clients) | ||
| { | ||
| var clientEntry = BuildClientEntry(client, optionsRef, localDefinitions); | ||
| clientProperties[client.Name] = clientEntry; | ||
| } | ||
|
|
||
| var schema = new JsonObject | ||
| { | ||
| ["$schema"] = "http://json-schema.org/draft-07/schema#", | ||
| ["type"] = "object", | ||
| ["properties"] = new JsonObject | ||
| { | ||
| [sectionName] = new JsonObject | ||
| { | ||
| ["type"] = "object", | ||
| ["properties"] = clientProperties, | ||
| ["additionalProperties"] = new JsonObject | ||
| { | ||
| ["type"] = "object", | ||
| ["description"] = "Configuration for a named client instance." | ||
| } | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| // Add local definitions only for types not covered by the base schema | ||
| if (localDefinitions.Count > 0) | ||
| { | ||
| var definitions = new JsonObject(); | ||
| foreach (var (name, definition) in localDefinitions.OrderBy(kvp => kvp.Key)) | ||
| { | ||
| definitions[name] = definition; | ||
| } | ||
| schema["definitions"] = definitions; | ||
| } | ||
|
|
||
| return schema; | ||
| } | ||
|
|
||
| private static JsonObject BuildClientEntry(ClientProvider client, string optionsRef, Dictionary<string, JsonObject> localDefinitions) | ||
| { | ||
| var settings = client.ClientSettings!; | ||
| var properties = new JsonObject(); | ||
|
|
||
| // Add endpoint property (Name is already transformed by PropertyProvider construction) | ||
| if (settings.EndpointProperty != null) | ||
| { | ||
| properties[settings.EndpointProperty.Name] = BuildPropertySchema(settings.EndpointProperty, localDefinitions); | ||
| } | ||
|
|
||
| // Add other required parameters (raw param names need ToIdentifierName() for PascalCase) | ||
| foreach (var param in settings.OtherRequiredParams) | ||
| { | ||
| var propName = param.Name.ToIdentifierName(); | ||
| properties[propName] = GetJsonSchemaForType(param.Type, localDefinitions); | ||
| } | ||
|
|
||
| // Add credential reference (defined in System.ClientModel base schema) | ||
| properties["Credential"] = new JsonObject | ||
| { | ||
| ["$ref"] = "#/definitions/credential" | ||
| }; | ||
|
|
||
| // Add options | ||
| properties["Options"] = BuildOptionsSchema(client, optionsRef, localDefinitions); | ||
|
|
||
| return new JsonObject | ||
| { | ||
| ["type"] = "object", | ||
| ["description"] = $"Configuration for {client.Name}.", | ||
| ["properties"] = properties | ||
| }; | ||
| } | ||
|
|
||
| private static JsonObject BuildOptionsSchema(ClientProvider client, string optionsRef, Dictionary<string, JsonObject> localDefinitions) | ||
| { | ||
| var clientOptions = client.EffectiveClientOptions; | ||
| if (clientOptions == null) | ||
| { | ||
| return new JsonObject | ||
| { | ||
| ["$ref"] = $"#/definitions/{optionsRef}" | ||
| }; | ||
| } | ||
|
|
||
| // Build a named local definition for this client's options type that inherits from the base options. | ||
| // This follows the same pattern used in the Azure emitter where client options types extend the | ||
| // core options type using allOf. | ||
| var optionsTypeName = clientOptions.Name; | ||
| var definitionName = optionsTypeName.Length > 1 | ||
| ? char.ToLowerInvariant(optionsTypeName[0]) + optionsTypeName.Substring(1) | ||
| : optionsTypeName.ToLowerInvariant(); | ||
|
|
||
| if (!localDefinitions.ContainsKey(definitionName)) | ||
| { | ||
| // Get client-specific option properties (public, non-version properties) | ||
| var customProperties = clientOptions.Properties | ||
| .Where(p => p.Modifiers.HasFlag(MethodSignatureModifiers.Public)) | ||
| .ToList(); | ||
|
|
||
| var allOfArray = new JsonArray | ||
| { | ||
| new JsonObject { ["$ref"] = $"#/definitions/{optionsRef}" } | ||
| }; | ||
|
|
||
| if (customProperties.Count > 0) | ||
| { | ||
| var extensionProperties = new JsonObject(); | ||
| foreach (var prop in customProperties) | ||
| { | ||
| extensionProperties[prop.Name] = GetJsonSchemaForType(prop.Type, localDefinitions); | ||
| } | ||
|
|
||
| allOfArray.Add(new JsonObject | ||
| { | ||
| ["type"] = "object", | ||
| ["properties"] = extensionProperties | ||
| }); | ||
| } | ||
|
|
||
| localDefinitions[definitionName] = new JsonObject | ||
| { | ||
| ["allOf"] = allOfArray | ||
| }; | ||
| } | ||
|
|
||
| return new JsonObject | ||
| { | ||
| ["$ref"] = $"#/definitions/{definitionName}" | ||
| }; | ||
| } | ||
|
|
||
| private static JsonObject BuildPropertySchema(PropertyProvider property, Dictionary<string, JsonObject> localDefinitions) | ||
| { | ||
| var schema = GetJsonSchemaForType(property.Type, localDefinitions); | ||
|
|
||
| if (property.Description != null) | ||
| { | ||
| var descriptionText = property.Description.ToString(); | ||
| if (!string.IsNullOrEmpty(descriptionText)) | ||
| { | ||
| schema["description"] = descriptionText; | ||
| } | ||
| } | ||
|
|
||
| return schema; | ||
| } | ||
|
|
||
| internal static JsonObject GetJsonSchemaForType(CSharpType type, Dictionary<string, JsonObject>? localDefinitions = null) | ||
| { | ||
| // Unwrap nullable types | ||
| var effectiveType = type.IsNullable ? type.WithNullable(false) : type; | ||
|
|
||
| // Handle non-framework types | ||
| if (!effectiveType.IsFrameworkType) | ||
| { | ||
| if (effectiveType.IsEnum) | ||
| { | ||
| return GetJsonSchemaForEnum(effectiveType, localDefinitions); | ||
| } | ||
|
|
||
| return GetJsonSchemaForModel(effectiveType, localDefinitions); | ||
| } | ||
|
|
||
| // Handle collection types | ||
| if (effectiveType.IsList) | ||
| { | ||
| return BuildArraySchema(effectiveType, localDefinitions); | ||
| } | ||
|
|
||
| var frameworkType = effectiveType.FrameworkType; | ||
|
|
||
| if (frameworkType == typeof(string)) | ||
| { | ||
| return new JsonObject { ["type"] = "string" }; | ||
| } | ||
| if (frameworkType == typeof(bool)) | ||
| { | ||
| return new JsonObject { ["type"] = "boolean" }; | ||
| } | ||
| if (frameworkType == typeof(int) || frameworkType == typeof(long)) | ||
| { | ||
| return new JsonObject { ["type"] = "integer" }; | ||
| } | ||
| if (frameworkType == typeof(float) || frameworkType == typeof(double)) | ||
| { | ||
| return new JsonObject { ["type"] = "number" }; | ||
| } | ||
| if (frameworkType == typeof(Uri)) | ||
| { | ||
| return new JsonObject { ["type"] = "string", ["format"] = "uri" }; | ||
| } | ||
| if (frameworkType == typeof(TimeSpan)) | ||
| { | ||
| return new JsonObject { ["type"] = "string" }; | ||
| } | ||
|
|
||
| return new JsonObject { ["type"] = "object" }; | ||
| } | ||
|
|
||
| private static JsonObject GetJsonSchemaForEnum(CSharpType enumType, Dictionary<string, JsonObject>? localDefinitions) | ||
| { | ||
| // Search both top-level and nested types (e.g., service version enums nested in options) in a single pass | ||
| var enumProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders | ||
| .SelectMany(t => new[] { t }.Concat(t.NestedTypes)) | ||
| .OfType<EnumProvider>() | ||
| .FirstOrDefault(e => e.Type.Equals(enumType)); | ||
|
|
||
| if (enumProvider != null) | ||
| { | ||
| var values = new JsonArray(); | ||
| foreach (var member in enumProvider.EnumValues) | ||
| { | ||
| values.Add(JsonValue.Create(member.Value?.ToString())); | ||
| } | ||
|
|
||
| JsonObject enumSchema; | ||
| if (enumType.IsStruct) | ||
| { | ||
| // Extensible enum — use anyOf to allow known values + custom strings | ||
| enumSchema = new JsonObject | ||
| { | ||
| ["anyOf"] = new JsonArray | ||
| { | ||
| new JsonObject { ["enum"] = values }, | ||
| new JsonObject { ["type"] = "string" } | ||
| } | ||
| }; | ||
| } | ||
| else | ||
| { | ||
| // Fixed enum | ||
| enumSchema = new JsonObject { ["enum"] = values }; | ||
| } | ||
|
|
||
| // Register as a local definition if we're collecting them | ||
| if (localDefinitions != null) | ||
| { | ||
| var name = enumProvider.Name; | ||
| var definitionName = name.Length > 1 | ||
| ? char.ToLowerInvariant(name[0]) + name.Substring(1) | ||
| : name.ToLowerInvariant(); | ||
| if (!localDefinitions.ContainsKey(definitionName)) | ||
| { | ||
| localDefinitions[definitionName] = enumSchema; | ||
| } | ||
| return new JsonObject { ["$ref"] = $"#/definitions/{definitionName}" }; | ||
| } | ||
|
|
||
| return enumSchema; | ||
| } | ||
|
|
||
| // Fallback: just string | ||
| return new JsonObject { ["type"] = "string" }; | ||
| } | ||
|
|
||
| private static JsonObject GetJsonSchemaForModel(CSharpType modelType, Dictionary<string, JsonObject>? localDefinitions) | ||
| { | ||
| // Search for the model provider in the output library | ||
| var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders | ||
| .SelectMany(t => new[] { t }.Concat(t.NestedTypes)) | ||
| .FirstOrDefault(m => m is ModelProvider && m.Type.Equals(modelType)); | ||
|
|
||
| if (modelProvider != null) | ||
| { | ||
| var name = modelProvider.Name; | ||
| var definitionName = name.Length > 1 | ||
| ? char.ToLowerInvariant(name[0]) + name.Substring(1) | ||
| : name.ToLowerInvariant(); | ||
|
|
||
| if (localDefinitions != null && !localDefinitions.ContainsKey(definitionName)) | ||
| { | ||
| var modelProperties = new JsonObject(); | ||
| foreach (var prop in modelProvider.Properties | ||
| .Where(p => p.Modifiers.HasFlag(MethodSignatureModifiers.Public))) | ||
| { | ||
| modelProperties[prop.Name] = GetJsonSchemaForType(prop.Type, localDefinitions); | ||
| } | ||
|
|
||
| var modelSchema = new JsonObject { ["type"] = "object" }; | ||
| if (modelProperties.Count > 0) | ||
| { | ||
| modelSchema["properties"] = modelProperties; | ||
| } | ||
|
|
||
| localDefinitions[definitionName] = modelSchema; | ||
| } | ||
|
|
||
| if (localDefinitions != null) | ||
| { | ||
| return new JsonObject { ["$ref"] = $"#/definitions/{definitionName}" }; | ||
| } | ||
| } | ||
|
|
||
| return new JsonObject { ["type"] = "object" }; | ||
| } | ||
|
|
||
| private static JsonObject BuildArraySchema(CSharpType listType, Dictionary<string, JsonObject>? localDefinitions) | ||
| { | ||
| if (listType.Arguments.Count > 0) | ||
| { | ||
| return new JsonObject | ||
| { | ||
| ["type"] = "array", | ||
| ["items"] = GetJsonSchemaForType(listType.Arguments[0], localDefinitions) | ||
| }; | ||
| } | ||
|
|
||
| return new JsonObject | ||
| { | ||
| ["type"] = "array", | ||
| ["items"] = new JsonObject { ["type"] = "string" } | ||
| }; | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot we need to handle the options type inheriting from the core type. Here is an example using Azure options https://github.com/Azure/azure-sdk-for-net/pull/57548/changes
Note we will have a separate PR to handle the Azure config schema, but we need to have the same pattern for the unbranded options.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Each client's options type is now modeled as a named local definition that inherits from the base
optionstype usingallOf. For example,SampleTypeSpecClientOptionsbecomes"sampleTypeSpecClientOptions": { "allOf": [{ "$ref": "#/definitions/options" }] }, and client-specific custom properties (when present) are added in a secondallOfelement. The client's"Options"property references this local definition via$ref. This matches the pattern from azure-sdk-for-net#57548. Fixed in 3de54bd.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot please add test cases where an options type has additional properties as compared to the base.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot we should also add a test case where an options property contains a model definition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added 3 new test cases in 06c5dba:
Generate_OptionsDefinition_IncludesStringProperty— options with a string additional propertyGenerate_OptionsDefinition_IncludesIntegerProperty— options with an integer additional propertyGenerate_OptionsDefinition_IncludesMultipleMixedProperties— options with string + boolean + integer additional propertiesEach test verifies the
allOfpattern has 2 elements (base$ref+ extension object with typed properties). All 17 tests pass.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added
GetJsonSchemaForModelthat looks up model providers in the output library and creates localdefinitionswith their properties (referenced via$ref). New testGenerate_OptionsDefinition_IncludesModelPropertycreates aRetryPolicyConfigmodel withMaxRetries(integer) andDelay(string) properties, then verifies the schema contains aretryPolicyConfigdefinition with those properties and the options extension references it via$ref. Fixed in 3db3324.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot let's also add a test where the model is a constructor parameter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added test
Generate_ConstructorParameter_IncludesModelDefinition— creates aConnectionConfigmodel (withHoststring andPortinteger properties) as a required constructor parameter. The test verifies that:definitionsentry (connectionConfig) with its properties$refto the definitionAll 19 tests pass. Fixed in 577eac7.