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 @@ -706,7 +706,7 @@ void AppendPublicConstructors(

private IEnumerable<ConstructorProvider> BuildSettingsConstructors()
{
if (ClientSettings == null || ClientSettings.EndpointPropertyName == null)
if (ClientSettings == null || ClientSettings.EndpointProperty == null)
{
yield break;
}
Expand All @@ -731,8 +731,8 @@ private IEnumerable<ConstructorProvider> BuildSettingsConstructors()
args.Add(Static(typeof(AuthenticationPolicy)).Invoke("Create", settingsParam));
#pragma warning restore SCME0002

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

// other required parameters (non-auth, non-endpoint) in primary constructor order
foreach (var param in ClientSettings.OtherRequiredParams)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,21 @@ internal ClientSettingsProvider(InputClient inputClient, ClientProvider clientPr

var inputEndpointParam = inputClient.Parameters
.FirstOrDefault(p => p is InputEndpointParameter ep && ep.IsEndpoint) as InputEndpointParameter;
EndpointPropertyName = inputEndpointParam?.Name.ToIdentifierName();

if (inputEndpointParam != null)
{
var endpointType = ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(inputEndpointParam.Type);
if (endpointType != null)
{
EndpointProperty = new PropertyProvider(
null,
MethodSignatureModifiers.Public,
endpointType.WithNullable(true),
inputEndpointParam.Name.ToIdentifierName(),
new AutoPropertyBody(true),
this);
}
}

// Collect non-endpoint, non-apiVersion required parameters (auth params come separately via InputClient.Auth)
OtherRequiredParams = inputClient.Parameters
Expand All @@ -49,7 +63,8 @@ internal ClientSettingsProvider(InputClient inputClient, ClientProvider clientPr
.ToList();
}

internal string? EndpointPropertyName { get; }
/// <summary>Gets the endpoint property (name and type) when the client has an endpoint parameter.</summary>
internal PropertyProvider? EndpointProperty { get; }

/// <summary>Gets non-endpoint, non-auth required parameters that have settings properties.</summary>
internal IReadOnlyList<ParameterProvider> OtherRequiredParams { get; }
Expand All @@ -74,15 +89,9 @@ protected override PropertyProvider[] BuildProperties()
{
var properties = new List<PropertyProvider>();

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

foreach (var param in OtherRequiredParams)
Expand Down Expand Up @@ -116,9 +125,9 @@ protected override MethodProvider[] BuildMethods()
var sectionParam = new ParameterProvider("section", $"The configuration section.", IConfigurationSectionType);
var body = new List<MethodBodyStatement>();

if (EndpointPropertyName != null)
if (EndpointProperty != null)
{
AppendUriTryCreateBinding(body, sectionParam, EndpointPropertyName, EndpointPropertyName.ToVariableName());
AppendBindingForProperty(body, sectionParam, EndpointProperty.Name, EndpointProperty.Name.ToVariableName(), EndpointProperty.Type);
}

foreach (var param in OtherRequiredParams)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.TypeSpec.Generator.ClientModel.Providers;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Primitives;
using Microsoft.TypeSpec.Generator.Providers;
using Microsoft.TypeSpec.Generator.Tests.Common;
using NUnit.Framework;

Expand Down Expand Up @@ -42,7 +43,7 @@ public void TestBaseType()
}

[Test]
public void TestProperties_WithEndpoint()
public void TestGeneratedSettings_WithStringEndpoint()
{
var inputParameters = new[]
{
Expand All @@ -59,13 +60,29 @@ public void TestProperties_WithEndpoint()

Assert.IsNotNull(settingsProvider);

// Validate Endpoint property is string? (not Uri?)
var properties = settingsProvider!.Properties;
// Should have Endpoint and Options properties
var endpointProp = properties.FirstOrDefault(p => p.Name == "Endpoint" && p.Type.Equals(new CSharpType(typeof(Uri), isNullable: true)));
Assert.IsNotNull(endpointProp, "Settings should have an Endpoint property of type Uri?");
var endpointProp = properties.FirstOrDefault(p => p.Name == "Endpoint" && p.Type.Equals(new CSharpType(typeof(string), isNullable: true)));
Assert.IsNotNull(endpointProp, "Settings should have an Endpoint property of type string?");

var optionsProp = properties.FirstOrDefault(p => p.Name == "Options");
Assert.IsNotNull(optionsProp, "Settings should have an Options property");

// Validate BindCore method
var bindCoreMethod = settingsProvider.Methods.FirstOrDefault(m => m.Signature.Name == "BindCore");
Assert.IsNotNull(bindCoreMethod, "Settings should have a BindCore method");
Assert.AreEqual(
MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override,
bindCoreMethod!.Signature.Modifiers);
Assert.AreEqual(1, bindCoreMethod.Signature.Parameters.Count);
Assert.AreEqual("section", bindCoreMethod.Signature.Parameters[0].Name);
var bodyString = bindCoreMethod.BodyStatements!.ToDisplayString();
Assert.IsTrue(bodyString.Contains("IsNullOrEmpty"), "BindCore should use string.IsNullOrEmpty for string endpoint binding");

// Validate full generated output
var writer = new TypeProviderWriter(settingsProvider);
var file = writer.Write();
Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
}

[Test]
Expand All @@ -83,14 +100,13 @@ public void TestProperties_NoEndpoint()
}

[Test]
public void TestBindCoreMethod_WithEndpoint()
public void TestGeneratedSettings_WithUrlEndpoint()
{
var inputParameters = new[]
{
InputFactory.EndpointParameter(
"endpoint",
InputPrimitiveType.String,
defaultValue: InputFactory.Constant.String("https://default.endpoint.io"),
InputPrimitiveType.Url,
scope: InputParameterScope.Client,
isEndpoint: true)
};
Expand All @@ -100,24 +116,21 @@ public void TestBindCoreMethod_WithEndpoint()

Assert.IsNotNull(settingsProvider);

var methods = settingsProvider!.Methods;
var bindCoreMethod = methods.FirstOrDefault(m => m.Signature.Name == "BindCore");
Assert.IsNotNull(bindCoreMethod, "Settings should have a BindCore method");

// Validate it's protected override
Assert.AreEqual(
MethodSignatureModifiers.Protected | MethodSignatureModifiers.Override,
bindCoreMethod!.Signature.Modifiers);
// Validate Endpoint property is Uri?
var properties = settingsProvider!.Properties;
var endpointProp = properties.FirstOrDefault(p => p.Name == "Endpoint" && p.Type.Equals(new CSharpType(typeof(Uri), isNullable: true)));
Assert.IsNotNull(endpointProp, "Settings should have an Endpoint property of type Uri?");

// Validate it has section parameter
Assert.AreEqual(1, bindCoreMethod.Signature.Parameters.Count);
Assert.AreEqual("section", bindCoreMethod.Signature.Parameters[0].Name);
// Validate BindCore uses Uri.TryCreate
var bindCoreMethod = settingsProvider.Methods.FirstOrDefault(m => m.Signature.Name == "BindCore");
Assert.IsNotNull(bindCoreMethod);
var bodyString = bindCoreMethod!.BodyStatements!.ToDisplayString();
Assert.IsTrue(bodyString.Contains("TryCreate"), "BindCore should use Uri.TryCreate for Uri endpoint binding");

// Validate the body contains Uri.TryCreate for endpoint binding
var body = bindCoreMethod.BodyStatements;
Assert.IsNotNull(body);
var bodyString = body!.ToDisplayString();
Assert.IsTrue(bodyString.Contains("TryCreate"), "BindCore should use Uri.TryCreate for endpoint binding");
// Validate full generated output
var writer = new TypeProviderWriter(settingsProvider);
var file = writer.Write();
Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
}

[Test]
Expand Down Expand Up @@ -690,7 +703,7 @@ public void TestSubClient_IndividuallyInitialized_HasEndpointProperty()

Assert.IsNotNull(settingsProvider);
var endpointProp = settingsProvider!.Properties.FirstOrDefault(
p => p.Name == "Endpoint" && p.Type.Equals(new CSharpType(typeof(Uri), isNullable: true)));
p => p.Name == "Endpoint" && p.Type.Equals(new CSharpType(typeof(string), isNullable: true)));
Assert.IsNotNull(endpointProp, "Sub-client settings should have an Endpoint property");
}

Expand Down Expand Up @@ -758,7 +771,7 @@ public void TestSubClient_IndividuallyInitialized_BindCoreHasEndpointAndOptions(
Assert.IsNotNull(bindCoreMethod, "Sub-client settings should have BindCore method");

var bodyString = bindCoreMethod!.BodyStatements!.ToDisplayString();
Assert.IsTrue(bodyString.Contains("TryCreate"), "BindCore should bind the Endpoint via Uri.TryCreate");
Assert.IsTrue(bodyString.Contains("IsNullOrEmpty"), "BindCore should bind the Endpoint via string.IsNullOrEmpty for string endpoint");
Assert.IsTrue(bodyString.Contains("GetSection") && bodyString.Contains("Options"),
"BindCore should bind the Options section");
}
Expand Down Expand Up @@ -790,5 +803,114 @@ public void TestSubClient_IndividuallyInitialized_SettingsBaseType()
Assert.AreEqual(ClientSettingsProvider.ClientSettingsType, settingsProvider!.Type.BaseType,
"Sub-client settings should inherit from ClientSettings");
}

[Test]
public void TestGeneratedSettings_WithNamedStringEndpoint()
{
var inputParameters = new[]
{
InputFactory.EndpointParameter(
"fullyQualifiedNamespace",
InputPrimitiveType.String,
scope: InputParameterScope.Client,
isEndpoint: true,
serverUrlTemplate: "https://{fullyQualifiedNamespace}")
};
var client = InputFactory.Client("TestClient", parameters: inputParameters);
var clientProvider = new ClientProvider(client);
var settingsProvider = clientProvider.ClientSettings;

Assert.IsNotNull(settingsProvider);

// Validate FullyQualifiedNamespace property is string? (not Uri?)
var properties = settingsProvider!.Properties;
var endpointProp = properties.FirstOrDefault(p => p.Name == "FullyQualifiedNamespace" && p.Type.Equals(new CSharpType(typeof(string), isNullable: true)));
Assert.IsNotNull(endpointProp, "Settings should have a FullyQualifiedNamespace property of type string?");

// Validate full generated output
var writer = new TypeProviderWriter(settingsProvider);
var file = writer.Write();
Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
}

[Test]
public void TestSettingsConstructor_WithStringEndpoint()
{
var inputParameters = new[]
{
InputFactory.EndpointParameter(
"endpoint",
InputPrimitiveType.String,
scope: InputParameterScope.Client,
isEndpoint: true,
serverUrlTemplate: "https://{endpoint}")
};
var client = InputFactory.Client("TestClient", parameters: inputParameters);
var clientProvider = new ClientProvider(client);

var settingsConstructor = clientProvider.Constructors.FirstOrDefault(IsSettingsConstructor);
Assert.IsNotNull(settingsConstructor, "Expected a settings constructor for string endpoint");

// Validate the initializer references the settings endpoint property
var initializer = settingsConstructor!.Signature.Initializer;
Assert.IsNotNull(initializer);
Assert.IsFalse(initializer!.IsBase, "Settings constructor should use this() initializer");

// The initializer should have arguments for auth policy, endpoint, and options
Assert.IsTrue(initializer.Arguments.Count >= 3,
"Settings constructor initializer should have at least 3 arguments (auth, endpoint, options)");

// Validate the endpoint argument references settings?.Endpoint
var endpointArg = initializer.Arguments[1].ToDisplayString();
Assert.IsTrue(endpointArg.Contains("Endpoint"),
$"Endpoint argument should reference Endpoint property, got: {endpointArg}");

// Validate full generated client output
var writer = new TypeProviderWriter(clientProvider);
var file = writer.Write();
Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
}

[Test]
public void TestSettingsConstructor_WithUrlEndpoint()
{
var inputParameters = new[]
{
InputFactory.EndpointParameter(
"endpoint",
InputPrimitiveType.Url,
scope: InputParameterScope.Client,
isEndpoint: true)
};
var client = InputFactory.Client("TestClient", parameters: inputParameters);
var clientProvider = new ClientProvider(client);

var settingsConstructor = clientProvider.Constructors.FirstOrDefault(IsSettingsConstructor);
Assert.IsNotNull(settingsConstructor, "Expected a settings constructor for URL endpoint");

// Validate the initializer references the settings endpoint property
var initializer = settingsConstructor!.Signature.Initializer;
Assert.IsNotNull(initializer);
Assert.IsFalse(initializer!.IsBase, "Settings constructor should use this() initializer");

// The initializer should have arguments for auth policy, endpoint, and options
Assert.IsTrue(initializer.Arguments.Count >= 3,
"Settings constructor initializer should have at least 3 arguments (auth, endpoint, options)");

// Validate the endpoint argument references settings?.Endpoint
var endpointArg = initializer.Arguments[1].ToDisplayString();
Assert.IsTrue(endpointArg.Contains("Endpoint"),
$"Endpoint argument should reference Endpoint property, got: {endpointArg}");

// Validate full generated client output
var writer = new TypeProviderWriter(clientProvider);
var file = writer.Write();
Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
}

private static bool IsSettingsConstructor(ConstructorProvider c) =>
c.Signature?.Initializer != null &&
c.Signature?.Modifiers == MethodSignatureModifiers.Public &&
c.Signature.Parameters.Any(p => p.Name == "settings");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// <auto-generated/>

#nullable disable

using System.ClientModel.Primitives;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Configuration;

namespace Sample
{
[global::System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SCME0002")]
public partial class TestClientSettings : global::System.ClientModel.Primitives.ClientSettings
{
public string FullyQualifiedNamespace { get; set; }

public global::Sample.TestClientOptions Options { get; set; }

protected override void BindCore(global::Microsoft.Extensions.Configuration.IConfigurationSection section)
{
string fullyQualifiedNamespace = section["FullyQualifiedNamespace"];
if (!string.IsNullOrEmpty(fullyQualifiedNamespace))
{
this.FullyQualifiedNamespace = fullyQualifiedNamespace;
}
global::Microsoft.Extensions.Configuration.IConfigurationSection optionsSection = section.GetSection("Options");
if (optionsSection.Exists())
{
this.Options = new global::Sample.TestClientOptions(optionsSection);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// <auto-generated/>

#nullable disable

using System.ClientModel.Primitives;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Configuration;

namespace Sample
{
[global::System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SCME0002")]
public partial class TestClientSettings : global::System.ClientModel.Primitives.ClientSettings
{
public string Endpoint { get; set; }

public global::Sample.TestClientOptions Options { get; set; }

protected override void BindCore(global::Microsoft.Extensions.Configuration.IConfigurationSection section)
{
string endpoint = section["Endpoint"];
if (!string.IsNullOrEmpty(endpoint))
{
this.Endpoint = endpoint;
}
global::Microsoft.Extensions.Configuration.IConfigurationSection optionsSection = section.GetSection("Options");
if (optionsSection.Exists())
{
this.Options = new global::Sample.TestClientOptions(optionsSection);
}
}
}
}
Loading
Loading