Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<ItemGroup>
<PackageReference Include="System.ClientModel" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.TypeSpec.Generator.Primitives;
using Microsoft.TypeSpec.Generator.Providers;
using Microsoft.TypeSpec.Generator.Shared;
using Microsoft.TypeSpec.Generator.Snippets;
using Microsoft.TypeSpec.Generator.Statements;
using Microsoft.TypeSpec.Generator.Utilities;
using static Microsoft.TypeSpec.Generator.Snippets.Snippet;
Expand Down Expand Up @@ -230,9 +231,15 @@ protected override TypeProvider[] BuildNestedTypes()

protected override ConstructorProvider[] BuildConstructors()
{
var configSectionCtor = BuildConfigurationSectionConstructor();

if (LatestVersionsFields is null)
{
return [];
var defaultCtor = new ConstructorProvider(
new ConstructorSignature(Type, $"Initializes a new instance of {_clientProvider.Name}Options.", MethodSignatureModifiers.Public, []),
MethodBodyStatement.Empty,
this);
return [defaultCtor, configSectionCtor];
}

var constructorBody = new List<MethodBodyStatement>();
Expand Down Expand Up @@ -281,7 +288,72 @@ protected override ConstructorProvider[] BuildConstructors()
new ConstructorSignature(Type, $"Initializes a new instance of {_clientProvider.Name}Options.", MethodSignatureModifiers.Public, constructorParameters),
constructorBody,
this);
return [constructor];
return [constructor, configSectionCtor];
}

private ConstructorProvider BuildConfigurationSectionConstructor()
{
var sectionParam = new ParameterProvider(
"section",
$"The configuration section.",
ClientSettingsProvider.IConfigurationSectionType);

var experimentalAttr = new AttributeStatement(
typeof(System.Diagnostics.CodeAnalysis.ExperimentalAttribute),
[Literal(ClientSettingsProvider.ClientSettingsDiagnosticId)]);

// Set version to latest version before the guard so it is always initialized
var body = new List<MethodBodyStatement>();
if (LatestVersionsFields != null && VersionProperties != null)
{
foreach (var (_, serviceVersionEnum) in LatestVersionsFields)
{
if (VersionProperties.TryGetValue(serviceVersionEnum, out var versionProperty))
{
var latestVersion = serviceVersionEnum.EnumValues[^1];
body.Add(versionProperty.Assign(Literal(latestVersion.Value)).Terminate());
}
}
}

// if (section is null || !section.Exists()) { return; }
var guardCondition = sectionParam.Is(Null).Or(Not(sectionParam.Invoke("Exists")));
var guardStatement = new IfStatement(guardCondition);
guardStatement.Add(Return());

body.Add(guardStatement);

// Build a set of version property names for O(1) lookup
var versionPropertyNames = VersionProperties?.Values.Select(vp => vp.Name).ToHashSet();

// Bind non-version properties from configuration
foreach (var property in Properties)
{
if (versionPropertyNames?.Contains(property.Name) == true)
{
continue;
}

// string? propValue = section["PropertyName"];
var propValueVar = new VariableExpression(new CSharpType(typeof(string), isNullable: true), $"{property.Name.ToVariableName()}FromConfig");
body.Add(Declare(propValueVar, new IndexerExpression(sectionParam, Literal(property.Name))));

// if (!string.IsNullOrEmpty(propValue)) { Property = propValue; }
var ifProp = new IfStatement(Not(Static(typeof(string)).Invoke("IsNullOrEmpty", propValueVar)));
ifProp.Add(This.Property(property.Name).Assign(propValueVar).Terminate());
body.Add(ifProp);
}

return new ConstructorProvider(
new ConstructorSignature(
Type,
$"Initializes a new instance of {_clientProvider.Name}Options from configuration.",
MethodSignatureModifiers.Internal,
[sectionParam],
attributes: [experimentalAttr],
initializer: new ConstructorInitializer(true, [sectionParam])),
new MethodBodyStatements([.. body]),
this);
}

protected override PropertyProvider[] BuildProperties()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
Expand Down Expand Up @@ -102,6 +103,7 @@ public ClientProvider(InputClient inputClient)
_publicCtorDescription = $"Initializes a new instance of {Name}.";
ClientOptions = _inputClient.Parent is null ? ClientOptionsProvider.CreateClientOptionsProvider(_inputClient, this) : null;
ClientOptionsParameter = ClientOptions != null ? ScmKnownParameters.ClientOptions(ClientOptions.Type) : null;
ClientSettings = ClientOptions != null ? new ClientSettingsProvider(_inputClient, this) : null;
IsMultiServiceClient = _inputClient.IsMultiServiceClient;

var apiKey = _inputAuth?.ApiKey;
Expand Down Expand Up @@ -362,6 +364,7 @@ private IReadOnlyList<ParameterProvider> GetClientParameters()
/// </summary>
public RestClientProvider RestClient => _restClient ??= new RestClientProvider(_inputClient, this);
public ClientOptionsProvider? ClientOptions { get; }
public ClientSettingsProvider? ClientSettings { get; }

public PropertyProvider PipelineProperty { get; }
public FieldProvider EndpointField { get; }
Expand Down Expand Up @@ -570,9 +573,11 @@ protected override ConstructorProvider[] BuildConstructors()

var shouldIncludeMockingConstructor = !onlyContainsUnsupportedAuth && secondaryConstructors.All(c => c.Signature.Parameters.Count > 0);

var settingsConstructors = BuildSettingsConstructors();

return shouldIncludeMockingConstructor
? [ConstructorProviderHelper.BuildMockingConstructor(this), .. secondaryConstructors, .. primaryConstructors]
: [.. secondaryConstructors, .. primaryConstructors];
? [ConstructorProviderHelper.BuildMockingConstructor(this), .. secondaryConstructors, .. primaryConstructors, .. settingsConstructors]
: [.. secondaryConstructors, .. primaryConstructors, .. settingsConstructors];

void AppendConstructors(
AuthFields? authFields,
Expand Down Expand Up @@ -612,6 +617,68 @@ void AppendConstructors(
}
}

private IEnumerable<ConstructorProvider> BuildSettingsConstructors()
{
if (ClientSettings == null || ClientSettings.EndpointPropertyName == null)
{
yield break;
}

var settingsParam = new ParameterProvider("settings", $"The settings for {Name}.", ClientSettings.Type);
var experimentalAttr = new AttributeStatement(typeof(ExperimentalAttribute), [Literal(ClientSettingsProvider.ClientSettingsDiagnosticId)]);

// Build the arguments for the this(...) initializer to call primary constructor
var args = new List<ValueExpression>();

// endpoint argument - we know EndpointPropertyName is not null at this point
args.Add(new MemberExpression(new NullConditionalExpression(settingsParam), ClientSettings.EndpointPropertyName));

// other required parameters (non-auth, non-endpoint) in primary constructor order
foreach (var param in ClientSettings.OtherRequiredParams)
{
var propName = param.Name.ToIdentifierName();
var propAccess = new MemberExpression(new NullConditionalExpression(settingsParam), propName);
// Value types (enums, primitives) need ?? default since null-conditional returns T?
ValueExpression arg = param.Type.IsValueType
? new BinaryOperatorExpression("??", propAccess, new KeywordExpression("default", null))
: propAccess;
args.Add(arg);
}

// credential argument
if (_oauth2Fields != null)
{
var credentialExpr = new MemberExpression(new NullConditionalExpression(settingsParam), "CredentialProvider");
args.Add(new AsExpression(credentialExpr, _oauth2Fields.AuthField.Type));
}
else if (_apiKeyAuthFields != null)
{
// settings?.Credential?.Key != null ? new ApiKeyCredential(settings?.Credential?.Key) : null
var credentialExpr = new MemberExpression(new NullConditionalExpression(settingsParam), "Credential");
var keyExpr = new MemberExpression(new NullConditionalExpression(credentialExpr), "Key");
var keyNotNull = new BinaryOperatorExpression("!=", keyExpr, Null);
var newApiKeyCredExpr = New.Instance(_apiKeyAuthFields.AuthField.Type,
new MemberExpression(new NullConditionalExpression(new MemberExpression(new NullConditionalExpression(settingsParam), "Credential")), "Key"));
args.Add(new TernaryConditionalExpression(keyNotNull, newApiKeyCredExpr, Null));
}

// options argument
args.Add(new MemberExpression(new NullConditionalExpression(settingsParam), "Options"));

var settingsConstructor = new ConstructorProvider(
new ConstructorSignature(
Type,
$"Initializes a new instance of {Name} from settings.",
MethodSignatureModifiers.Public,
[settingsParam],
attributes: [experimentalAttr],
initializer: new ConstructorInitializer(false, args)),
MethodBodyStatement.Empty,
this);

yield return settingsConstructor;
}

private void AppendSubClientPublicConstructors(List<ConstructorProvider> constructors)
{
// For sub-clients that can be initialized individually, we need to create public constructors
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Microsoft.Extensions.Configuration;
using Microsoft.TypeSpec.Generator.Expressions;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Input.Extensions;
using Microsoft.TypeSpec.Generator.Primitives;
using Microsoft.TypeSpec.Generator.Providers;
using Microsoft.TypeSpec.Generator.Statements;
using static Microsoft.TypeSpec.Generator.Snippets.Snippet;

namespace Microsoft.TypeSpec.Generator.ClientModel.Providers
{
public class ClientSettingsProvider : TypeProvider
{
internal const string ClientSettingsDiagnosticId = "SCME0002";

private readonly ClientProvider _clientProvider;
private readonly InputEndpointParameter? _inputEndpointParam;
private readonly IReadOnlyList<ParameterProvider> _otherRequiredParams;

#pragma warning disable SCME0002 // ClientSettings is for evaluation purposes only
internal static readonly CSharpType ClientSettingsType = typeof(ClientSettings);
#pragma warning restore SCME0002

internal static readonly CSharpType IConfigurationSectionType = typeof(IConfigurationSection);

internal ClientSettingsProvider(InputClient inputClient, ClientProvider clientProvider)
{
_clientProvider = clientProvider;
_inputEndpointParam = inputClient.Parameters
.FirstOrDefault(p => p is InputEndpointParameter ep && ep.IsEndpoint) as InputEndpointParameter;

// Collect non-endpoint, non-apiVersion required parameters (auth params come separately via InputClient.Auth)
_otherRequiredParams = inputClient.Parameters
.Where(p => p.IsRequired && !p.IsApiVersion &&
!(p is InputEndpointParameter ep && ep.IsEndpoint))
.Select(p => ScmCodeModelGenerator.Instance.TypeFactory.CreateParameter(p))
.Where(p => p != null)
.Select(p => p!)
.ToList();
}

internal string? EndpointPropertyName => _inputEndpointParam?.Name.ToIdentifierName();

/// <summary>Gets non-endpoint, non-auth required parameters that have settings properties.</summary>
internal IReadOnlyList<ParameterProvider> OtherRequiredParams => _otherRequiredParams;

protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", $"{Name}.cs");

protected override string BuildName() => $"{_clientProvider.Name}Settings";

protected override string BuildNamespace() => _clientProvider.Type.Namespace;

protected override CSharpType BuildBaseType() => ClientSettingsType;

protected override IReadOnlyList<MethodBodyStatement> BuildAttributes()
{
return [new AttributeStatement(typeof(ExperimentalAttribute), Literal(ClientSettingsDiagnosticId))];
}

protected override PropertyProvider[] BuildProperties()
{
var properties = new List<PropertyProvider>();

if (_inputEndpointParam != null)
{
properties.Add(new PropertyProvider(
null,
MethodSignatureModifiers.Public,
new CSharpType(typeof(Uri), isNullable: true),
EndpointPropertyName!,
new AutoPropertyBody(true),
this));
}

foreach (var param in _otherRequiredParams)
{
properties.Add(new PropertyProvider(
null,
MethodSignatureModifiers.Public,
param.Type.WithNullable(true),
param.Name.ToIdentifierName(),
new AutoPropertyBody(true),
this));
}

if (_clientProvider.ClientOptions != null)
{
properties.Add(new PropertyProvider(
null,
MethodSignatureModifiers.Public,
_clientProvider.ClientOptions.Type.WithNullable(true),
"Options",
new AutoPropertyBody(true),
this));
}

return [.. properties];
}

protected override MethodProvider[] BuildMethods()
{
var sectionParam = new ParameterProvider("section", $"The configuration section.", IConfigurationSectionType);
var body = new List<MethodBodyStatement>();

if (_inputEndpointParam != null)
{
var endpointPropertyName = EndpointPropertyName!;

// string? endpoint = section["EndpointPropertyName"];
var endpointVar = new VariableExpression(new CSharpType(typeof(string), isNullable: true), "endpoint");
body.Add(Declare(endpointVar, new IndexerExpression(sectionParam, Literal(endpointPropertyName))));

// if (!string.IsNullOrEmpty(endpoint)) { EndpointProperty = new Uri(endpoint); }
var ifStatement = new IfStatement(Not(Static(typeof(string)).Invoke("IsNullOrEmpty", endpointVar)));
ifStatement.Add(This.Property(endpointPropertyName).Assign(New.Instance(typeof(Uri), endpointVar)).Terminate());
body.Add(ifStatement);
}

foreach (var param in _otherRequiredParams)
{
var propName = param.Name.ToIdentifierName();
// For string types: if (section[propName] is string val) PropName = val;
if (param.Type.IsFrameworkType && param.Type.FrameworkType == typeof(string))
{
var valVar = new VariableExpression(new CSharpType(typeof(string), isNullable: true), param.Name.ToVariableName());
body.Add(Declare(valVar, new IndexerExpression(sectionParam, Literal(propName))));
var ifStatement = new IfStatement(Not(Static(typeof(string)).Invoke("IsNullOrEmpty", valVar)));
ifStatement.Add(This.Property(propName).Assign(valVar).Terminate());
body.Add(ifStatement);
}
// Other types are skipped in BindCore (users can customize via partial class)
}

if (_clientProvider.ClientOptions != null)
{
// IConfigurationSection optionsSection = section.GetSection("Options");
var optionsSectionVar = new VariableExpression(IConfigurationSectionType, "optionsSection");
body.Add(Declare(optionsSectionVar, sectionParam.Invoke("GetSection", Literal("Options"))));

// if (optionsSection.Exists()) { Options = new ClientOptions(optionsSection); }
var ifOptionsStatement = new IfStatement(optionsSectionVar.Invoke("Exists"));
ifOptionsStatement.Add(This.Property("Options").Assign(
New.Instance(_clientProvider.ClientOptions.Type, optionsSectionVar)).Terminate());
body.Add(ifOptionsStatement);
}

var bindCoreMethod = new MethodProvider(
new MethodSignature(
"BindCore",
$"Binds configuration values from the given section.",
MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override,
null,
null,
[sectionParam]),
new MethodBodyStatements([.. body]),
this);

return [bindCoreMethod];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ private static void BuildClient(InputClient inputClient, HashSet<TypeProvider> t
if (clientOptions != null)
{
types.Add(clientOptions);
var clientSettings = client.ClientSettings;
if (clientSettings != null)
{
types.Add(clientSettings);
}
}

// We use the spec view methods so that we include collection definitions even if the user is customizing or suppressing
Expand Down
Loading