Skip to content
Merged
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 @@ -105,7 +105,10 @@ 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;
bool isIndividuallyInitialized = (_inputClient.InitializedBy & InputClientInitializedBy.Individually) != 0;
ClientSettings = isIndividuallyInitialized
? new ClientSettingsProvider(_inputClient, this)
: null;
IsMultiServiceClient = _inputClient.IsMultiServiceClient;

var apiKey = _inputAuth?.ApiKey;
Expand All @@ -132,8 +135,7 @@ public ClientProvider(InputClient inputClient)
this,
initializationValue: Literal(apiKey.Prefix)) :
null;
// skip auth fields for sub-clients
_apiKeyAuthFields = ClientOptions is null ? null : new(apiKeyAuthField, authorizationHeaderField, authorizationApiKeyPrefixField);
_apiKeyAuthFields = isIndividuallyInitialized ? new(apiKeyAuthField, authorizationHeaderField, authorizationApiKeyPrefixField) : null;
}

var tokenAuth = _inputAuth?.OAuth2;
Expand All @@ -157,8 +159,7 @@ public ClientProvider(InputClient inputClient)

var tokenCredentialScopesField = BuildTokenCredentialScopesField(tokenAuth, tokenCredentialType);

// skip auth fields for sub-clients
_oauth2Fields = ClientOptions is null ? null : new(tokenCredentialField, tokenCredentialScopesField);
_oauth2Fields = isIndividuallyInitialized ? new(tokenCredentialField, tokenCredentialScopesField) : null;
}
EndpointField = new(
FieldModifiers.Private | FieldModifiers.ReadOnly,
Expand Down Expand Up @@ -299,14 +300,8 @@ private IReadOnlyList<ParameterProvider> GetSubClientInternalConstructorParamete
PipelineProperty.AsParameter
};

if (_apiKeyAuthFields != null)
{
subClientParameters.Add(_apiKeyAuthFields.AuthField.AsParameter);
}
if (_oauth2Fields != null)
{
subClientParameters.Add(_oauth2Fields.AuthField.AsParameter);
}
// Auth credentials are NOT included here — the parent passes its authenticated
// pipeline, so the sub-client doesn't need separate credential parameters.
subClientParameters.Add(_endpointParameter);
subClientParameters.AddRange(ClientParameters);

Expand Down Expand Up @@ -384,6 +379,12 @@ private IReadOnlyList<ParameterProvider> GetClientParameters()
public ClientOptionsProvider? ClientOptions { get; }
public ClientSettingsProvider? ClientSettings { get; }

/// <summary>
/// Gets the effective <see cref="ClientOptionsProvider"/> — the client's own options for root clients,
/// or the root client's options for individually-initialized sub-clients.
/// </summary>
internal ClientOptionsProvider? EffectiveClientOptions => ClientOptions ?? GetRootClient()?.ClientOptions;

public PropertyProvider PipelineProperty { get; }
public FieldProvider EndpointField { get; }

Expand Down Expand Up @@ -646,7 +647,9 @@ void AppendPublicConstructors(
foreach (var p in requiredParameters)
{
if (authParamName == null || p.Name != authParamName)
{
initializerArgs.Add(p);
}
}
initializerArgs.Add(ClientOptionsParameter!);

Expand Down Expand Up @@ -685,6 +688,14 @@ private IEnumerable<ConstructorProvider> BuildSettingsConstructors()
yield break;
}

// Only publicly constructible clients should get the Settings constructor.
// Internal clients (e.g., those made internal via custom code) cannot be
// constructed by consumers, so a public Settings constructor is not useful.
if (!DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public))
{
yield break;
}

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

Expand Down Expand Up @@ -732,64 +743,108 @@ private IEnumerable<ConstructorProvider> BuildSettingsConstructors()
private void AppendSubClientPublicConstructors(List<ConstructorProvider> constructors)
{
// For sub-clients that can be initialized individually, we need to create public constructors
// similar to the root client constructors but adapted for sub-client needs
// with the same auth pattern as the root client.
var primaryConstructors = new List<ConstructorProvider>();
var secondaryConstructors = new List<ConstructorProvider>();

// if there is key auth
var rootClient = GetRootClient();
var clientOptionsParameter = rootClient?.ClientOptionsParameter;
var clientOptionsProvider = rootClient?.ClientOptions;

if (clientOptionsParameter == null || clientOptionsProvider == null)
{
return;
}

// Add the internal AuthenticationPolicy constructor first — public constructors chain to it.
var authPolicyParam = new ParameterProvider(
"authenticationPolicy",
$"The authentication policy to use for pipeline creation.",
new CSharpType(typeof(AuthenticationPolicy), isNullable: true));

var requiredNonAuthParams = GetRequiredParameters(null);
ParameterProvider[] internalConstructorParameters = [authPolicyParam, _endpointParameter, .. requiredNonAuthParams, clientOptionsParameter];

var internalConstructor = new ConstructorProvider(
new ConstructorSignature(Type, _publicCtorDescription, MethodSignatureModifiers.Internal, internalConstructorParameters),
BuildPrimaryConstructorBody(internalConstructorParameters, null, authPolicyParam, clientOptionsProvider, clientOptionsParameter, addExplicitValidation: true),
this);
primaryConstructors.Add(internalConstructor);

// Add public constructors with auth — same pattern as root client
if (_apiKeyAuthFields != null)
{
AppendSubClientPublicConstructorsForAuth(_apiKeyAuthFields, primaryConstructors, secondaryConstructors);
}
// if there is oauth2 auth
if (_oauth2Fields != null)
{
AppendSubClientPublicConstructorsForAuth(_oauth2Fields, primaryConstructors, secondaryConstructors);
}

// if there is no auth
bool onlyContainsUnsupportedAuth = _inputAuth != null && _apiKeyAuthFields == null && _oauth2Fields == null;
if (_apiKeyAuthFields == null && _oauth2Fields == null)
{
AppendSubClientPublicConstructorsForAuth(null, primaryConstructors, secondaryConstructors);
AppendSubClientPublicConstructorsForAuth(null, primaryConstructors, secondaryConstructors, onlyContainsUnsupportedAuth);
}

constructors.AddRange(secondaryConstructors);
constructors.AddRange(primaryConstructors);

// Add Settings constructor for individually-initialized sub-clients
foreach (var settingsConstructor in BuildSettingsConstructors())
{
constructors.Add(settingsConstructor);
}

void AppendSubClientPublicConstructorsForAuth(
AuthFields? authFields,
List<ConstructorProvider> primaryConstructors,
List<ConstructorProvider> secondaryConstructors)
{
// For a sub-client with individual initialization, we need:
// - endpoint parameter
// - auth parameter (if auth exists)
// - client options parameter (we need to get this from the root client)
var rootClient = GetRootClient();
var clientOptionsParameter = rootClient?.ClientOptionsParameter;
var clientOptionsProvider = rootClient?.ClientOptions;
if (clientOptionsParameter == null || clientOptionsProvider == null)
List<ConstructorProvider> secondaryConstructors,
bool onlyContainsUnsupportedAuth = false)
{
// Public constructor with credential parameter — delegates to the internal constructor via this(...).
var requiredParameters = GetRequiredParameters(authFields?.AuthField);
ParameterProvider[] primaryConstructorParameters = [_endpointParameter, .. requiredParameters, clientOptionsParameter];
var constructorModifier = onlyContainsUnsupportedAuth ? MethodSignatureModifiers.Internal : MethodSignatureModifiers.Public;

// Build the auth policy expression for the this() initializer
ValueExpression authPolicyArg = BuildAuthPolicyArgument(authFields, requiredParameters);
var initializerArgs = new List<ValueExpression> { authPolicyArg, _endpointParameter };
string? authParamName = authFields != null
? (authFields.AuthField.Name != TokenProviderFieldName ? CredentialParamName : authFields.AuthField.AsParameter.Name)
: null;
foreach (var p in requiredParameters)
{
// Cannot create public constructor without client options
return;
if (authParamName == null || p.Name != authParamName)
{
initializerArgs.Add(p);
}
}
initializerArgs.Add(clientOptionsParameter!);

var requiredParameters = GetRequiredParameters(authFields?.AuthField);
ParameterProvider[] primaryConstructorParameters = [_endpointParameter, .. requiredParameters, clientOptionsParameter];
var primaryConstructor = new ConstructorProvider(
new ConstructorSignature(Type, _publicCtorDescription, MethodSignatureModifiers.Public, primaryConstructorParameters),
BuildPrimaryConstructorBody(primaryConstructorParameters, authFields, null, clientOptionsProvider, clientOptionsParameter),
new ConstructorSignature(Type, _publicCtorDescription, constructorModifier, primaryConstructorParameters,
initializer: new ConstructorInitializer(false, initializerArgs)),
MethodBodyStatement.Empty,
this);

primaryConstructors.Add(primaryConstructor);

// If the endpoint parameter contains an initialization value, it is not required.
ParameterProvider[] secondaryConstructorParameters = _endpointParameter.InitializationValue is null
? [_endpointParameter, .. requiredParameters]
: [.. requiredParameters];
var secondaryConstructor = BuildSecondaryConstructor(secondaryConstructorParameters, primaryConstructorParameters, MethodSignatureModifiers.Public);
var secondaryConstructor = BuildSecondaryConstructor(secondaryConstructorParameters, primaryConstructorParameters, constructorModifier);

secondaryConstructors.Add(secondaryConstructor);

// When endpoint has a default value and there are required parameters,
// add an additional constructor that accepts required parameters + options.
if (_endpointParameter.InitializationValue is not null && requiredParameters.Count > 0)
{
ParameterProvider[] simplifiedConstructorWithOptionsParameters = [.. requiredParameters, clientOptionsParameter];
var simplifiedConstructorWithOptions = BuildSecondaryConstructor(simplifiedConstructorWithOptionsParameters, primaryConstructorParameters, constructorModifier);
secondaryConstructors.Add(simplifiedConstructorWithOptions);
}
}
}

Expand Down Expand Up @@ -916,11 +971,18 @@ private MethodBodyStatement[] BuildPrimaryConstructorBody(IReadOnlyList<Paramete
}

ValueExpression perRetryPolicies;
if (authPolicyParam != null && authFields != null)
if (authPolicyParam != null)
{
// Internal implementation constructor: use the authenticationPolicy parameter directly
perRetryPoliciesList.Add(authPolicyParam);
perRetryPolicies = New.Array(ScmCodeModelGenerator.Instance.TypeFactory.ClientPipelineApi.PipelinePolicyType, isInline: true, [.. perRetryPoliciesList]);
// Internal implementation constructor: generate a runtime null check for the auth policy.
// No-auth clients pass null, so we must guard against adding null to the policies array.
var pipelinePolicyType = ScmCodeModelGenerator.Instance.TypeFactory.ClientPipelineApi.PipelinePolicyType;
var perRetryWithoutAuth = New.Array(pipelinePolicyType, isInline: true, [.. perRetryPoliciesList]);
var perRetryWithAuth = New.Array(pipelinePolicyType, isInline: true, [.. perRetryPoliciesList, authPolicyParam]);

body.Add(new IfElseStatement(
authPolicyParam.NotEqual(Null),
PipelineProperty.Assign(This.ToApi<ClientPipelineApi>().Create(clientOptionsParameter, perRetryWithAuth)).Terminate(),
PipelineProperty.Assign(This.ToApi<ClientPipelineApi>().Create(clientOptionsParameter, perRetryWithoutAuth)).Terminate()));
}
else
{
Expand All @@ -939,9 +1001,9 @@ private MethodBodyStatement[] BuildPrimaryConstructorBody(IReadOnlyList<Paramete
perRetryPolicies = New.Array(ScmCodeModelGenerator.Instance.TypeFactory.ClientPipelineApi.PipelinePolicyType, isInline: true, [.. perRetryPoliciesList]);
break;
}
}

body.Add(PipelineProperty.Assign(This.ToApi<ClientPipelineApi>().Create(clientOptionsParameter, perRetryPolicies)).Terminate());
body.Add(PipelineProperty.Assign(This.ToApi<ClientPipelineApi>().Create(clientOptionsParameter, perRetryPolicies)).Terminate());
}

foreach (var f in Fields)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,13 @@ protected override PropertyProvider[] BuildProperties()
this));
}

if (_clientProvider.ClientOptions != null)
var clientOptions = _clientProvider.EffectiveClientOptions;
if (clientOptions != null)
{
properties.Add(new PropertyProvider(
null,
MethodSignatureModifiers.Public,
_clientProvider.ClientOptions.Type.WithNullable(true),
clientOptions.Type.WithNullable(true),
"Options",
new AutoPropertyBody(true),
this));
Expand All @@ -126,9 +127,10 @@ protected override MethodProvider[] BuildMethods()
AppendBindingForProperty(body, sectionParam, propName, param.Name.ToVariableName(), param.Type);
}

if (_clientProvider.ClientOptions != null)
var clientOptions = _clientProvider.EffectiveClientOptions;
if (clientOptions != null)
{
AppendComplexObjectBinding(body, sectionParam, "Options", "options", _clientProvider.ClientOptions.Type);
AppendComplexObjectBinding(body, sectionParam, "Options", "options", clientOptions.Type);
}

var bindCoreMethod = new MethodProvider(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using Microsoft.TypeSpec.Generator.ClientModel.Providers;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Primitives;
using Microsoft.TypeSpec.Generator.Providers;

namespace Microsoft.TypeSpec.Generator.ClientModel
Expand Down Expand Up @@ -41,11 +42,13 @@ 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);
}
}

// Emit the Settings class for any publicly constructible client (root or individually-initialized sub-client).
var clientSettings = client.ClientSettings;
if (clientSettings != null && client.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public))
{
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
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ public async Task CanRenameSubClient()
]);
var inputServiceMethod = InputFactory.BasicServiceMethod("test", inputOperation);
var inputClient = InputFactory.Client("TestClient", methods: [inputServiceMethod]);
InputClient subClient = InputFactory.Client("custom", parent: inputClient);
InputClient subClient = InputFactory.Client("custom", parent: inputClient, initializedBy: InputClientInitializedBy.Parent);
var mockGenerator = await MockHelpers.LoadMockGeneratorAsync(
clients: () => [inputClient],
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());
Expand Down Expand Up @@ -309,7 +309,7 @@ public async Task CanRemoveCachingField()
]);
var inputServiceMethod = InputFactory.BasicServiceMethod("test", inputOperation);
var inputClient = InputFactory.Client("TestClient", methods: [inputServiceMethod]);
InputClient subClient = InputFactory.Client("dog", methods: [], parameters: [], parent: inputClient);
InputClient subClient = InputFactory.Client("dog", methods: [], parameters: [], parent: inputClient, initializedBy: InputClientInitializedBy.Parent);
var mockGenerator = await MockHelpers.LoadMockGeneratorAsync(
clients: () => [inputClient],
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());
Expand Down
Loading
Loading