From ac1c9557935babe2150488cf30747cacea186045 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Tue, 20 Jan 2026 17:48:45 -0600 Subject: [PATCH] feat: add support for multisvc client --- .../emitter/src/lib/client-converter.ts | 10 +- .../test/Unit/client-converter.test.ts | 206 +++++++++++++++++ .../src/Primitives/ScmKnownParameters.cs | 2 +- .../src/Providers/ClientOptionsProvider.cs | 212 ++++++++++++------ .../src/Providers/ClientProvider.cs | 110 +++++++-- .../src/Providers/RestClientProvider.cs | 7 +- .../Providers/ClientOptionsProviderTests.cs | 86 ++++++- .../ClientProviders/ClientProviderTests.cs | 114 ++++++++++ ...tiServiceClient_GeneratesExpectedClient.cs | 56 +++++ ...thThreeServices_GeneratesExpectedClient.cs | 65 ++++++ ...ceClient_GeneratesExpectedClientOptions.cs | 47 ++++ ...Services_GeneratesExpectedClientOptions.cs | 64 ++++++ .../src/InputLibrary.cs | 15 ++ .../src/InputTypes/InputClient.cs | 1 + .../src/Primitives/PropertyWireInformation.cs | 5 +- .../src/Providers/ApiVersionEnumProvider.cs | 30 ++- .../src/Providers/CanonicalTypeProvider.cs | 2 + .../src/Providers/EnumProvider.cs | 3 + .../src/Shared/ClientHelper.cs | 39 ++++ .../Providers/CanonicalTypeProviderTests.cs | 4 +- .../ApiVersionEnumProviderTests.cs | 68 ++++++ .../test/Providers/FieldProviderTests.cs | 2 +- .../test/Shared/ClientHelperTests.cs | 171 ++++++++++++++ .../test/Shared/MethodSignatureHelperTests.cs | 2 +- .../test/TestHelpers/MockHelpers.cs | 4 + 25 files changed, 1222 insertions(+), 103 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/MultiServiceClient_GeneratesExpectedClient.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/MultiServiceClient_WithThreeServices_GeneratesExpectedClient.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/MultiServiceClient_GeneratesExpectedClientOptions.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/MultiServiceClient_WithThreeServices_GeneratesExpectedClientOptions.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/ClientHelper.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Shared/ClientHelperTests.cs diff --git a/packages/http-client-csharp/emitter/src/lib/client-converter.ts b/packages/http-client-csharp/emitter/src/lib/client-converter.ts index ff75e604ce2..0099b5331ae 100644 --- a/packages/http-client-csharp/emitter/src/lib/client-converter.ts +++ b/packages/http-client-csharp/emitter/src/lib/client-converter.ts @@ -63,9 +63,15 @@ function fromSdkClient( client.namespace, ); + const isMultiService = isMultiServiceClient(client); + const clientName = + !client.parent && isMultiService && !client.name.toLowerCase().endsWith("client") + ? `${client.name}Client` + : client.name; + inputClient = { kind: "client", - name: client.name, + name: clientName, namespace: client.namespace, doc: client.doc, summary: client.summary, @@ -79,7 +85,7 @@ function fromSdkClient( apiVersions: client.apiVersions, parent: undefined, children: undefined, - isMultiServiceClient: isMultiServiceClient(client), + isMultiServiceClient: isMultiService, }; sdkContext.__typeCache.updateSdkClientReferences(client, inputClient); diff --git a/packages/http-client-csharp/emitter/test/Unit/client-converter.test.ts b/packages/http-client-csharp/emitter/test/Unit/client-converter.test.ts index ab683388071..49d80ee6033 100644 --- a/packages/http-client-csharp/emitter/test/Unit/client-converter.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/client-converter.test.ts @@ -251,3 +251,209 @@ describe("isMultiServiceClient", () => { } }); }); + +describe("client name suffix", () => { + let runner: TestHost; + + beforeEach(async () => { + runner = await createEmitterTestHost(); + }); + + it("should append Client suffix to multi-service root client without suffix", async () => { + const program = await typeSpecCompile( + ` + @versioned(VersionsA) + namespace ServiceA { + enum VersionsA { + av1, + } + + @route("/test") + op testOne(@query("api-version") apiVersion: VersionsA): void; + } + + @versioned(VersionsB) + namespace ServiceB { + enum VersionsB { + bv1, + } + + @route("/test") + op testTwo(@query("api-version") apiVersion: VersionsB): void; + } + + @client({ + name: "Combined", + service: [ServiceA, ServiceB], + }) + namespace Service.MultiService {} + `, + runner, + { IsNamespaceNeeded: false, IsTCGCNeeded: true }, + ); + const context = createEmitterContext(program); + const sdkContext = await createCSharpSdkContext(context); + const root = createModel(sdkContext); + + const client = root.clients[0]; + ok(client, "Client should exist"); + strictEqual( + client.name, + "CombinedClient", + "Multi-service root client should have Client suffix appended", + ); + }); + + it("should not duplicate Client suffix for multi-service root client already ending with Client", async () => { + const program = await typeSpecCompile( + ` + @versioned(VersionsA) + namespace ServiceA { + enum VersionsA { + av1, + } + + @route("/test") + op testOne(@query("api-version") apiVersion: VersionsA): void; + } + + @versioned(VersionsB) + namespace ServiceB { + enum VersionsB { + bv1, + } + + @route("/test") + op testTwo(@query("api-version") apiVersion: VersionsB): void; + } + + @client({ + name: "CombinedClient", + service: [ServiceA, ServiceB], + }) + namespace Service.MultiService {} + `, + runner, + { IsNamespaceNeeded: false, IsTCGCNeeded: true }, + ); + const context = createEmitterContext(program); + const sdkContext = await createCSharpSdkContext(context); + const root = createModel(sdkContext); + + const client = root.clients[0]; + ok(client, "Client should exist"); + strictEqual( + client.name, + "CombinedClient", + "Multi-service root client already ending with Client should not have suffix duplicated", + ); + }); + + it("should not duplicate Client suffix with different casing", async () => { + const program = await typeSpecCompile( + ` + @versioned(VersionsA) + namespace ServiceA { + enum VersionsA { + av1, + } + + @route("/test") + op testOne(@query("api-version") apiVersion: VersionsA): void; + } + + @versioned(VersionsB) + namespace ServiceB { + enum VersionsB { + bv1, + } + + @route("/test") + op testTwo(@query("api-version") apiVersion: VersionsB): void; + } + + @client({ + name: "CombinedCLIENT", + service: [ServiceA, ServiceB], + }) + namespace Service.MultiService {} + `, + runner, + { IsNamespaceNeeded: false, IsTCGCNeeded: true }, + ); + const context = createEmitterContext(program); + const sdkContext = await createCSharpSdkContext(context); + const root = createModel(sdkContext); + + const client = root.clients[0]; + ok(client, "Client should exist"); + strictEqual( + client.name, + "CombinedCLIENT", + "Multi-service root client ending with CLIENT (uppercase) should not have suffix duplicated", + ); + }); + + it("should not append Client suffix to sub-clients of multi-service client", async () => { + const program = await typeSpecCompile( + ` + @versioned(VersionsA) + namespace ServiceA { + enum VersionsA { + av1, + } + + @route("/a") + interface AI { + @route("test") + op aTest(): void; + } + } + + @versioned(VersionsB) + namespace ServiceB { + enum VersionsB { + bv1, + } + + @route("/b") + interface BI { + @route("test") + op bTest(): void; + } + } + + @client({ + name: "Combined", + service: [ServiceA, ServiceB], + }) + @useDependency(ServiceA.VersionsA.av1, ServiceB.VersionsB.bv1) + namespace Service.MultiService {} + `, + runner, + { IsNamespaceNeeded: false, IsTCGCNeeded: true }, + ); + const context = createEmitterContext(program); + const sdkContext = await createCSharpSdkContext(context); + const root = createModel(sdkContext); + + const client = root.clients[0]; + ok(client, "Client should exist"); + strictEqual( + client.name, + "CombinedClient", + "Multi-service root client should have Client suffix appended", + ); + + // Verify sub-clients do NOT have Client suffix appended + ok(client.children, "Client should have children"); + ok(client.children.length > 0, "Client should have at least one child"); + for (const childClient of client.children) { + strictEqual( + childClient.name.endsWith("Client"), + false, + `Child client '${childClient.name}' should not have Client suffix`, + ); + } + }); +}); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Primitives/ScmKnownParameters.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Primitives/ScmKnownParameters.cs index 0ccf88d30e2..bfbefd24fbb 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Primitives/ScmKnownParameters.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Primitives/ScmKnownParameters.cs @@ -74,7 +74,7 @@ public static ParameterProvider ClientOptions(CSharpType clientOptionsType) DefaultValue = Static(typeof(DateTimeOffset)).Property(nameof(DateTimeOffset.Now)) }; - public static readonly ParameterProvider ContentType = new("contentType", $"The contentType to use which has the multipart/form-data boundary.", typeof(string), wireInfo: new PropertyWireInformation(SerializationFormat.Default, true, false, false, false, "Content-Type", false)); + public static readonly ParameterProvider ContentType = new("contentType", $"The contentType to use which has the multipart/form-data boundary.", typeof(string), wireInfo: new PropertyWireInformation(SerializationFormat.Default, true, false, false, false, "Content-Type", false, false)); public static readonly ParameterProvider NextPage = new ParameterProvider("nextPage", $"The url of the next page of responses.", typeof(Uri)); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientOptionsProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientOptionsProvider.cs index 717131ad42b..7001641f55e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientOptionsProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientOptionsProvider.cs @@ -10,6 +10,8 @@ using Microsoft.TypeSpec.Generator.Input.Extensions; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.Shared; +using Microsoft.TypeSpec.Generator.Statements; using Microsoft.TypeSpec.Generator.Utilities; using static Microsoft.TypeSpec.Generator.Snippets.Snippet; @@ -17,34 +19,43 @@ namespace Microsoft.TypeSpec.Generator.ClientModel.Providers { public class ClientOptionsProvider : TypeProvider { - private const string LatestVersionFieldName = "LatestVersion"; - private const string VersionPropertyName = "Version"; + private const string ServicePrefix = "Service"; + private const string VersionSuffix = "Version"; + private const string ApiVersionSuffix = "ApiVersion"; + private const string LatestPrefix = "Latest"; + private const string LatestVersionFieldName = $"{LatestPrefix}{VersionSuffix}"; + private readonly InputClient _inputClient; private readonly ClientProvider _clientProvider; - private readonly TypeProvider? _serviceVersionEnum; - private readonly PropertyProvider? _versionProperty; - private FieldProvider? _latestVersionField; + private readonly Dictionary? _serviceVersionsEnums; private static ClientOptionsProvider? _singletonInstance; - // Internal constructor for testing purposes internal ClientOptionsProvider(InputClient inputClient, ClientProvider clientProvider) { _inputClient = inputClient; _clientProvider = clientProvider; - var inputEnumType = ScmCodeModelGenerator.Instance.InputLibrary.InputNamespace.Enums - .FirstOrDefault(e => e.Usage.HasFlag(InputModelTypeUsage.ApiVersionEnum)); - if (inputEnumType != null) + List inputEnums = [.. ScmCodeModelGenerator.Instance.InputLibrary.InputNamespace.Enums + .Where(e => e.Usage.HasFlag(InputModelTypeUsage.ApiVersionEnum))]; + + if (inputEnums.Count > 0) { - _serviceVersionEnum = ScmCodeModelGenerator.Instance.TypeFactory.CreateEnum(inputEnumType, this); - // Ensure the service version enum uses the same namespace as the options class since it is nested. - _serviceVersionEnum?.Update(@namespace: Type.Namespace); - _versionProperty = new( - null, - MethodSignatureModifiers.Internal, - typeof(string), - VersionPropertyName, - new AutoPropertyBody(false), - this); + _serviceVersionsEnums = []; + foreach (var inputEnum in inputEnums) + { + var enumProvider = ScmCodeModelGenerator.Instance.TypeFactory.CreateEnum(inputEnum, this); + if (enumProvider != null) + { + // Ensure the service version enum uses the same namespace as the options class since it is nested. + enumProvider.Update(@namespace: Type.Namespace); + _serviceVersionsEnums.Add(inputEnum, enumProvider); + } + + // Only create one version property for single service clients + if (!_inputClient.IsMultiServiceClient) + { + break; + } + } } } @@ -110,20 +121,60 @@ private static bool UseSingletonInstance(InputClient inputClient) return true; } - internal PropertyProvider? VersionProperty => _versionProperty; - private FieldProvider? LatestVersionField => _latestVersionField ??= BuildLatestVersionField(); + internal IReadOnlyDictionary? VersionProperties => field ??= BuildVersionProperties(); - private FieldProvider? BuildLatestVersionField() + private Dictionary? BuildVersionProperties() { - if (_serviceVersionEnum == null) + if (_serviceVersionsEnums is null) + { return null; + } - return new( - modifiers: FieldModifiers.Private | FieldModifiers.Const, - type: _serviceVersionEnum.Type, - name: LatestVersionFieldName, - enclosingType: this, - initializationValue: Static(_serviceVersionEnum.Type).Property(_serviceVersionEnum.EnumValues[^1].Name)); + var properties = new Dictionary(_serviceVersionsEnums.Count); + foreach (var (inputEnum, enumProvider) in _serviceVersionsEnums) + { + var versionPropertyName = _inputClient.IsMultiServiceClient + ? ClientHelper.BuildNameForService(inputEnum.Namespace, ServicePrefix, ApiVersionSuffix) + : VersionSuffix; + + var versionProperty = new PropertyProvider( + null, + MethodSignatureModifiers.Internal, + typeof(string), + versionPropertyName, + new AutoPropertyBody(false), + this); + properties.Add(enumProvider, versionProperty); + } + + return properties; + } + private IReadOnlyDictionary? LatestVersionsFields => field ??= BuildLatestVersionsFields(); + + private Dictionary? BuildLatestVersionsFields() + { + if (_serviceVersionsEnums is null) + { + return null; + } + + Dictionary latestVersionFields = new(_serviceVersionsEnums.Count); + foreach (var enumProvider in _serviceVersionsEnums.Values) + { + var fieldName = _inputClient.IsMultiServiceClient + ? $"{LatestPrefix}{enumProvider.Name.ToIdentifierName()}" + : LatestVersionFieldName; + var field = new FieldProvider( + modifiers: FieldModifiers.Private | FieldModifiers.Const, + type: enumProvider.Type, + name: fieldName, + enclosingType: this, + initializationValue: Static(enumProvider.Type).Property(enumProvider.EnumValues[^1].Name)); + + latestVersionFields.Add(field, enumProvider); + } + + return latestVersionFields; } protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", $"{Name}.cs"); @@ -159,60 +210,97 @@ protected override CSharpType BuildBaseType() protected override FieldProvider[] BuildFields() { - if (LatestVersionField == null) + if (LatestVersionsFields is null) + { return []; + } - return [LatestVersionField]; + return [.. LatestVersionsFields.Keys.OrderBy(f => f.Name)]; } protected override TypeProvider[] BuildNestedTypes() { - if (_serviceVersionEnum == null) + if (_serviceVersionsEnums is null) + { return []; + } - return [_serviceVersionEnum]; + return [.. _serviceVersionsEnums.Values.OrderBy(e => e.Name)]; } protected override ConstructorProvider[] BuildConstructors() { - if (_serviceVersionEnum == null || LatestVersionField == null) + if (LatestVersionsFields is null) + { return []; + } - var versionParam = new ParameterProvider( - "version", - $"The service version", - _serviceVersionEnum.Type, - defaultValue: LatestVersionField); - var serviceVersionsCount = _serviceVersionEnum.EnumValues.Count; - List switchCases = new(serviceVersionsCount + 1); - - for (int i = 0; i < serviceVersionsCount; i++) + var constructorBody = new List(); + var constructorParameters = new List(); + foreach (var (latestVersionField, serviceVersionEnum) in LatestVersionsFields) { - var serviceVersionMember = _serviceVersionEnum.EnumValues[i]; - // ServiceVersion.Version => "version" - switchCases.Add(new( - Static(_serviceVersionEnum.Type).Property(serviceVersionMember.Name), - new LiteralExpression(serviceVersionMember.Value))); - } + if (VersionProperties is null || + !VersionProperties.TryGetValue(serviceVersionEnum, out PropertyProvider? versionProperty)) + { + continue; + } - switchCases.Add(SwitchCaseExpression.Default(ThrowExpression(New.NotSupportedException(ValueExpression.Empty)))); + string versionParameterName = "version"; + FormattableString versionParamDescription = $"The service version"; + if (_inputClient.IsMultiServiceClient) + { + versionParameterName = ClientHelper.BuildNameForService( + serviceVersionEnum.Name, + ServicePrefix, + VersionSuffix).ToVariableName(); + versionParamDescription = $"The {serviceVersionEnum.Name} service version"; + } + + var versionParam = new ParameterProvider( + versionParameterName, + versionParamDescription, + serviceVersionEnum.Type, + defaultValue: latestVersionField); + constructorParameters.Add(versionParam); + + var enumValues = serviceVersionEnum.EnumValues; + var switchCases = new List(enumValues.Count + 1); + foreach (var serviceVersionMember in enumValues) + { + // ServiceVersion.Version => "version" + switchCases.Add(new SwitchCaseExpression( + Static(serviceVersionEnum.Type).Property(serviceVersionMember.Name), + new LiteralExpression(serviceVersionMember.Value))); + } + + switchCases.Add(SwitchCaseExpression.Default(ThrowExpression(New.NotSupportedException(ValueExpression.Empty)))); + constructorBody.Add(versionProperty.Assign(new SwitchExpression(versionParam, [.. switchCases])).Terminate()); + } var constructor = new ConstructorProvider( - new ConstructorSignature(Type, $"Initializes a new instance of {_clientProvider.Name}Options.", MethodSignatureModifiers.Public, [versionParam]), - _versionProperty!.Assign(new SwitchExpression(versionParam, [.. switchCases])).Terminate(), + new ConstructorSignature(Type, $"Initializes a new instance of {_clientProvider.Name}Options.", MethodSignatureModifiers.Public, constructorParameters), + constructorBody, this); return [constructor]; } protected override PropertyProvider[] BuildProperties() { - List properties = _versionProperty != null ? [_versionProperty] : []; + var properties = VersionProperties is not null + ? [.. VersionProperties.Values.OrderBy(p => p.Name)] + : new List(); foreach (var p in _inputClient.Parameters) { if ((p is not InputEndpointParameter || p is InputEndpointParameter endpointParameter && !endpointParameter.IsEndpoint) && !p.IsApiVersion && p.DefaultValue != null) { + var type = ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(p.Type)?.PropertyInitializationType; + if (type is null) + { + continue; + } + FormattableString? description = null; var parameterDescription = DocHelpers.GetDescription(p.Summary, p.Doc); if (parameterDescription is not null) @@ -220,17 +308,13 @@ protected override PropertyProvider[] BuildProperties() description = $"{parameterDescription}"; } - var type = ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(p.Type)?.PropertyInitializationType; - if (type != null) - { - properties.Add(new( - description, - MethodSignatureModifiers.Public, - type, - p.Name.ToIdentifierName(), - new AutoPropertyBody(true), - this)); - } + properties.Add(new PropertyProvider( + description, + MethodSignatureModifiers.Public, + type, + p.Name.ToIdentifierName(), + new AutoPropertyBody(true), + this)); } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs index 864ae37ac8e..5356421d44b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ClientProvider.cs @@ -26,6 +26,7 @@ public class ClientProvider : TypeProvider private record AuthFields(FieldProvider AuthField); private record ApiKeyFields(FieldProvider AuthField, FieldProvider AuthorizationHeaderField, FieldProvider? AuthorizationApiKeyPrefixField) : AuthFields(AuthField); private record OAuth2Fields(FieldProvider AuthField, FieldProvider AuthorizationScopesField) : AuthFields(AuthField); + private record ApiVersionFields(FieldProvider Field, PropertyProvider? CorrespondingOptionsProperty, string? ServiceNamespace); private const string AuthorizationHeaderConstName = "AuthorizationHeader"; private const string AuthorizationApiKeyPrefixConstName = "AuthorizationApiKeyPrefix"; @@ -48,12 +49,12 @@ private record OAuth2Fields(FieldProvider AuthField, FieldProvider Authorization private readonly ApiKeyFields? _apiKeyAuthFields; private readonly OAuth2Fields? _oauth2Fields; - private FieldProvider? _apiVersionField; + private IReadOnlyList? _apiVersionFields; private readonly Lazy> _subClientInternalConstructorParams; private readonly Lazy> _subClients; private RestClientProvider? _restClient; private readonly IReadOnlyList _allClientParameters; - private Lazy> _additionalClientFields; + private readonly Lazy> _additionalClientFields; private Dictionary? _methodCache; private Dictionary MethodCache => _methodCache ??= []; @@ -382,7 +383,8 @@ protected override FieldProvider[] BuildFields() private IReadOnlyList BuildAdditionalClientFields() { var fields = new List(); - // Add optional client parameters as fields + bool builtApiVersionFields = false; + foreach (var p in _allClientParameters) { if (p is not InputEndpointParameter || p is InputEndpointParameter endpointParameter && !endpointParameter.IsEndpoint) @@ -391,32 +393,87 @@ private IReadOnlyList BuildAdditionalClientFields() ? ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(enumType.ValueType) : ScmCodeModelGenerator.Instance.TypeFactory.CreateCSharpType(p.Type); - if (type != null) + if (type is null) { - FieldProvider field = new( + continue; + } + + var wireInfo = new PropertyWireInformation( + ScmCodeModelGenerator.Instance.TypeFactory.GetSerializationFormat(p.Type), + p.IsRequired, + false, + p.Type is InputNullableType, + false, + p.SerializedName, + false, + p.IsApiVersion); + + if (p.IsApiVersion && !builtApiVersionFields) + { + _apiVersionFields = BuildApiVersionFields(p, type, wireInfo); + fields.AddRange(_apiVersionFields.Select(f => f.Field).OrderBy(f => f.Name)); + builtApiVersionFields = true; + } + else + { + var field = new FieldProvider( FieldModifiers.Private | FieldModifiers.ReadOnly, type.WithNullable(!p.IsRequired), "_" + p.Name.ToVariableName(), this, - wireInfo: new PropertyWireInformation( - ScmCodeModelGenerator.Instance.TypeFactory.GetSerializationFormat(p.Type), - p.IsRequired, - false, - p.Type is InputNullableType, - false, - p.SerializedName, - false)); - if (p.IsApiVersion) - { - _apiVersionField = field; - } + wireInfo: wireInfo); fields.Add(field); } } } + return fields; } + private List BuildApiVersionFields( + InputParameter inputParameter, + CSharpType type, + PropertyWireInformation wireInfo) + { + var fieldType = type.WithNullable(!inputParameter.IsRequired); + string fieldName = "_" + inputParameter.Name.ToVariableName(); + + if (ClientOptions?.VersionProperties is { } versionProperties) + { + var propertyCount = versionProperties.Count; + var fields = new List(propertyCount); + + foreach (var (enumProvider, property) in versionProperties) + { + var name = propertyCount > 1 + ? "_" + property.Name.ToVariableName() + : fieldName; + + var field = new FieldProvider( + FieldModifiers.Private | FieldModifiers.ReadOnly, + fieldType, + name, + this, + wireInfo: wireInfo); + + fields.Add(new ApiVersionFields(field, property, enumProvider.InputNamespace)); + } + + return fields; + } + else + { + var field = new FieldProvider( + FieldModifiers.Private | FieldModifiers.ReadOnly, + fieldType, + fieldName, + this, + wireInfo: wireInfo); + + return [new ApiVersionFields(field, null, null)]; + } + } + protected override PropertyProvider[] BuildProperties() { return [PipelineProperty]; @@ -709,16 +766,12 @@ private MethodBodyStatement[] BuildPrimaryConstructorBody(IReadOnlyList().Create(clientOptionsParameter, perRetryPolicies)).Terminate()); - var clientOptionsPropertyDict = clientOptionsProvider.Properties.ToDictionary(p => p.Name.ToIdentifierName()); foreach (var f in Fields) { - if (f == _apiVersionField && clientOptionsProvider.VersionProperty != null) - { - body.Add(f.Assign(clientOptionsParameter.Property(clientOptionsProvider.VersionProperty.Name)).Terminate()); - } - else if (clientOptionsPropertyDict.TryGetValue(f.Name.ToIdentifierName(), out var optionsProperty)) + var fieldInfo = _apiVersionFields?.FirstOrDefault(fieldInfo => fieldInfo.Field == f); + if (fieldInfo?.CorrespondingOptionsProperty != null) { - clientOptionsPropertyDict.TryGetValue(f.Name.ToIdentifierName(), out optionsProperty); + body.Add(f.Assign(clientOptionsParameter.Property(fieldInfo.CorrespondingOptionsProperty.Name)).Terminate()); } } @@ -810,6 +863,15 @@ protected override ScmMethodProvider[] BuildMethods() { subClientConstructorArgs.Add(parentField); } + else if (param.Field?.WireInfo?.IsApiVersion == true) + { + var correspondingApiVersionField = _apiVersionFields? + .FirstOrDefault(fieldData => fieldData.ServiceNamespace?.Equals(subClient._inputClient.Namespace) == true); + if (correspondingApiVersionField != null) + { + subClientConstructorArgs.Add(correspondingApiVersionField.Field); + } + } } // Create the interlocked compare exchange expression for the body diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs index 5cd4afef759..69504c9925b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/RestClientProvider.cs @@ -49,7 +49,7 @@ public RestClientProvider(InputClient inputClient, ClientProvider clientProvider protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", $"{Name}.RestClient.cs"); - protected override string BuildName() => _inputClient.Name.ToIdentifierName(); + protected override string BuildName() => ClientProvider.Name; protected override string BuildNamespace() => ClientProvider.Type.Namespace; @@ -921,6 +921,11 @@ internal static List GetMethodParameters(InputServiceMethod s continue; } + if (inputParam.IsApiVersion && inputParam.DefaultValue != null) + { + continue; + } + ParameterProvider? parameter = ScmCodeModelGenerator.Instance.TypeFactory.CreateParameter(inputParam)?.ToPublicInputParameter(); if (parameter is null) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientOptionsProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientOptionsProviderTests.cs index 4418263a58e..950c380e226 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientOptionsProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientOptionsProviderTests.cs @@ -8,13 +8,10 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.TypeSpec.Generator.ClientModel.Providers; -using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Tests.Common; -using Moq; -using Moq.Protected; using NUnit.Framework; namespace Microsoft.TypeSpec.Generator.ClientModel.Tests.Providers @@ -444,5 +441,88 @@ public void NamespaceLastSegmentIsUsedForSingletonName() // Note: InputNamespace in MockHelpers is set to "Sample" by default, not based on client namespace Assert.AreEqual("SampleClientOptions", options!.Name); } + + [Test] + public void MultiServiceClient_GeneratesExpectedClientOptions() + { + // Setup multiservice client with multiple API version enums + List serviceAVersions = ["1.0", "2.0"]; + List serviceBVersions = ["3.0", "4.0"]; + + var serviceAEnumValues = serviceAVersions.Select(a => (a, a)); + var serviceBEnumValues = serviceBVersions.Select(a => (a, a)); + + var serviceAEnum = InputFactory.StringEnum( + "ServiceVersionA", + serviceAEnumValues, + usage: InputModelTypeUsage.ApiVersionEnum, + clientNamespace: "Sample.ServiceA"); + var serviceBEnum = InputFactory.StringEnum( + "ServiceVersionB", + serviceBEnumValues, + usage: InputModelTypeUsage.ApiVersionEnum, + clientNamespace: "Sample.ServiceB"); + var client = InputFactory.Client("TestClient", isMultiServiceClient: true); + + MockHelpers.LoadMockGenerator( + apiVersions: () => [.. serviceAVersions, .. serviceBVersions], + clients: () => [client], + inputEnums: () => [serviceAEnum, serviceBEnum]); + + var clientProvider = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(client); + var clientOptionsProvider = clientProvider?.ClientOptions; + + Assert.IsNotNull(clientOptionsProvider); + + var writer = new TypeProviderWriter(clientOptionsProvider!); + var file = writer.Write(); + + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + [Test] + public void MultiServiceClient_WithThreeServices_GeneratesExpectedClientOptions() + { + // Setup multiservice client with three different services (KeyVault, Storage, Compute) + List keyVaultVersions = ["7.4", "7.5"]; + List storageVersions = ["2023-01-01", "2024-01-01"]; + List computeVersions = ["2023-07-01", "2024-03-01", "2024-07-01"]; + + var keyVaultEnumValues = keyVaultVersions.Select(a => (a, a)); + var storageEnumValues = storageVersions.Select(a => (a, a)); + var computeEnumValues = computeVersions.Select(a => (a, a)); + + var keyVaultEnum = InputFactory.StringEnum( + "KeyVaultVersion", + keyVaultEnumValues, + usage: InputModelTypeUsage.ApiVersionEnum, + clientNamespace: "Sample.KeyVault"); + var storageEnum = InputFactory.StringEnum( + "StorageVersion", + storageEnumValues, + usage: InputModelTypeUsage.ApiVersionEnum, + clientNamespace: "Sample.Storage"); + var computeEnum = InputFactory.StringEnum( + "ComputeVersion", + computeEnumValues, + usage: InputModelTypeUsage.ApiVersionEnum, + clientNamespace: "Sample.Compute"); + var client = InputFactory.Client("TestClient", isMultiServiceClient: true); + + MockHelpers.LoadMockGenerator( + apiVersions: () => [.. keyVaultVersions, .. storageVersions, .. computeVersions], + clients: () => [client], + inputEnums: () => [keyVaultEnum, storageEnum, computeEnum]); + + var clientProvider = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(client); + var clientOptionsProvider = clientProvider?.ClientOptions; + + Assert.IsNotNull(clientOptionsProvider); + + var writer = new TypeProviderWriter(clientOptionsProvider!); + var file = writer.Write(); + + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs index a2b1caf8ed1..608568da59e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/ClientProviderTests.cs @@ -2916,5 +2916,119 @@ public void ServerTemplateEqualsEndpoint_OnlyAppendsOperationPath() Assert.IsTrue(fullText.Contains("/items"), "Should append the operation path /items"); } + + [Test] + public void MultiServiceClient_GeneratesExpectedClient() + { + // Setup multiservice client with multiple API version enums + List serviceAVersions = ["1.0", "2.0"]; + List serviceBVersions = ["3.0", "4.0"]; + + var serviceAEnumValues = serviceAVersions.Select(a => (a, a)); + var serviceBEnumValues = serviceBVersions.Select(a => (a, a)); + + var serviceAEnum = InputFactory.StringEnum( + "ServiceVersionA", + serviceAEnumValues, + usage: InputModelTypeUsage.ApiVersionEnum, + clientNamespace: "Sample.ServiceA"); + var serviceBEnum = InputFactory.StringEnum( + "ServiceVersionB", + serviceBEnumValues, + usage: InputModelTypeUsage.ApiVersionEnum, + clientNamespace: "Sample.ServiceB"); + + InputParameter apiVersionParameter = InputFactory.PathParameter( + "apiVersion", + InputPrimitiveType.String, + isRequired: true, + scope: InputParameterScope.Client, + isApiVersion: true); + + InputParameter subscriptionIdParameter = InputFactory.PathParameter( + "subscriptionId", + InputPrimitiveType.String, + isRequired: true, + scope: InputParameterScope.Client); + + var client = InputFactory.Client(TestClientName, parameters: [subscriptionIdParameter, apiVersionParameter], isMultiServiceClient: true); + var serviceAClient = InputFactory.Client("ServiceA", clientNamespace: "Sample.ServiceA", parent: client, parameters: [apiVersionParameter, subscriptionIdParameter]); + var serviceBClient = InputFactory.Client("ServiceB", clientNamespace: "Sample.ServiceB", parent: client, parameters: [apiVersionParameter, subscriptionIdParameter]); + + MockHelpers.LoadMockGenerator( + apiVersions: () => [.. serviceAVersions, .. serviceBVersions], + clients: () => [client, serviceAClient, serviceBClient], + inputEnums: () => [serviceAEnum, serviceBEnum]); + + var clientProvider = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(client); + + Assert.IsNotNull(clientProvider); + + var writer = new TypeProviderWriter(clientProvider!); + var file = writer.Write(); + + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + [Test] + public void MultiServiceClient_WithThreeServices_GeneratesExpectedClient() + { + // Setup multiservice client with three different services (KeyVault, Storage, Compute) + List keyVaultVersions = ["7.4", "7.5"]; + List storageVersions = ["2023-01-01", "2024-01-01"]; + List computeVersions = ["2023-07-01", "2024-03-01", "2024-07-01"]; + + var keyVaultEnumValues = keyVaultVersions.Select(a => (a, a)); + var storageEnumValues = storageVersions.Select(a => (a, a)); + var computeEnumValues = computeVersions.Select(a => (a, a)); + + var keyVaultEnum = InputFactory.StringEnum( + "KeyVaultVersion", + keyVaultEnumValues, + usage: InputModelTypeUsage.ApiVersionEnum, + clientNamespace: "Sample.KeyVault"); + var storageEnum = InputFactory.StringEnum( + "StorageVersion", + storageEnumValues, + usage: InputModelTypeUsage.ApiVersionEnum, + clientNamespace: "Sample.Storage"); + var computeEnum = InputFactory.StringEnum( + "ComputeVersion", + computeEnumValues, + usage: InputModelTypeUsage.ApiVersionEnum, + clientNamespace: "Sample.Compute"); + + InputParameter apiVersionParameter = InputFactory.PathParameter( + "apiVersion", + InputPrimitiveType.String, + isRequired: true, + scope: InputParameterScope.Client, + isApiVersion: true); + + InputParameter subscriptionIdParameter = InputFactory.PathParameter( + "subscriptionId", + InputPrimitiveType.String, + isRequired: true, + scope: InputParameterScope.Client); + + var client = InputFactory.Client(TestClientName, parameters: [subscriptionIdParameter, apiVersionParameter], isMultiServiceClient: true); + var keyVaultClient = InputFactory.Client("KeyVault", clientNamespace: "Sample.KeyVault", parent: client, parameters: [apiVersionParameter, subscriptionIdParameter]); + var storageClient = InputFactory.Client("Storage", clientNamespace: "Sample.Storage", parent: client, parameters: [apiVersionParameter, subscriptionIdParameter]); + var computeClient = InputFactory.Client("Compute", clientNamespace: "Sample.Compute", parent: client, parameters: [apiVersionParameter, subscriptionIdParameter]); + + MockHelpers.LoadMockGenerator( + apiVersions: () => [.. keyVaultVersions, .. storageVersions, .. computeVersions], + clients: () => [client, keyVaultClient, storageClient, computeClient], + inputEnums: () => [keyVaultEnum, storageEnum, computeEnum]); + + var clientProvider = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(client); + + Assert.IsNotNull(clientProvider); + + var writer = new TypeProviderWriter(clientProvider!); + var file = writer.Write(); + + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/MultiServiceClient_GeneratesExpectedClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/MultiServiceClient_GeneratesExpectedClient.cs new file mode 100644 index 00000000000..b650c53a695 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/MultiServiceClient_GeneratesExpectedClient.cs @@ -0,0 +1,56 @@ +// + +#nullable disable + +using System; +using System.ClientModel.Primitives; +using System.Threading; +using Sample.ServiceA; +using Sample.ServiceB; + +namespace Sample +{ + public partial class TestClient + { + private readonly global::System.Uri _endpoint; + private readonly string _subscriptionId; + private readonly string _serviceAApiVersion; + private readonly string _serviceBApiVersion; + private global::Sample.ServiceA.ServiceA _cachedServiceA; + private global::Sample.ServiceB.ServiceB _cachedServiceB; + + protected TestClient() + { + } + + public TestClient(global::System.Uri endpoint, string subscriptionId) : this(endpoint, subscriptionId, new global::Sample.TestClientOptions()) + { + } + + public TestClient(global::System.Uri endpoint, string subscriptionId, global::Sample.TestClientOptions options) + { + global::Sample.Argument.AssertNotNull(endpoint, nameof(endpoint)); + global::Sample.Argument.AssertNotNullOrEmpty(subscriptionId, nameof(subscriptionId)); + + options ??= new global::Sample.TestClientOptions(); + + _endpoint = endpoint; + _subscriptionId = subscriptionId; + Pipeline = global::System.ClientModel.Primitives.ClientPipeline.Create(options, Array.Empty(), new global::System.ClientModel.Primitives.PipelinePolicy[] { new global::System.ClientModel.Primitives.UserAgentPolicy(typeof(global::Sample.TestClient).Assembly) }, Array.Empty()); + _serviceAApiVersion = options.ServiceAApiVersion; + _serviceBApiVersion = options.ServiceBApiVersion; + } + + public global::System.ClientModel.Primitives.ClientPipeline Pipeline { get; } + + public virtual global::Sample.ServiceA.ServiceA GetServiceAClient() + { + return (global::System.Threading.Volatile.Read(ref _cachedServiceA) ?? (global::System.Threading.Interlocked.CompareExchange(ref _cachedServiceA, new global::Sample.ServiceA.ServiceA(Pipeline, _endpoint, _serviceAApiVersion, _subscriptionId), null) ?? _cachedServiceA)); + } + + public virtual global::Sample.ServiceB.ServiceB GetServiceBClient() + { + return (global::System.Threading.Volatile.Read(ref _cachedServiceB) ?? (global::System.Threading.Interlocked.CompareExchange(ref _cachedServiceB, new global::Sample.ServiceB.ServiceB(Pipeline, _endpoint, _serviceBApiVersion, _subscriptionId), null) ?? _cachedServiceB)); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/MultiServiceClient_WithThreeServices_GeneratesExpectedClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/MultiServiceClient_WithThreeServices_GeneratesExpectedClient.cs new file mode 100644 index 00000000000..ae8e7b8d8ae --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ClientProviders/TestData/ClientProviderTests/MultiServiceClient_WithThreeServices_GeneratesExpectedClient.cs @@ -0,0 +1,65 @@ +// + +#nullable disable + +using System; +using System.ClientModel.Primitives; +using System.Threading; +using Sample.Compute; +using Sample.KeyVault; +using Sample.Storage; + +namespace Sample +{ + public partial class TestClient + { + private readonly global::System.Uri _endpoint; + private readonly string _subscriptionId; + private readonly string _serviceComputeApiVersion; + private readonly string _serviceKeyVaultApiVersion; + private readonly string _serviceStorageApiVersion; + private global::Sample.KeyVault.KeyVault _cachedKeyVault; + private global::Sample.Storage.Storage _cachedStorage; + private global::Sample.Compute.Compute _cachedCompute; + + protected TestClient() + { + } + + public TestClient(global::System.Uri endpoint, string subscriptionId) : this(endpoint, subscriptionId, new global::Sample.TestClientOptions()) + { + } + + public TestClient(global::System.Uri endpoint, string subscriptionId, global::Sample.TestClientOptions options) + { + global::Sample.Argument.AssertNotNull(endpoint, nameof(endpoint)); + global::Sample.Argument.AssertNotNullOrEmpty(subscriptionId, nameof(subscriptionId)); + + options ??= new global::Sample.TestClientOptions(); + + _endpoint = endpoint; + _subscriptionId = subscriptionId; + Pipeline = global::System.ClientModel.Primitives.ClientPipeline.Create(options, Array.Empty(), new global::System.ClientModel.Primitives.PipelinePolicy[] { new global::System.ClientModel.Primitives.UserAgentPolicy(typeof(global::Sample.TestClient).Assembly) }, Array.Empty()); + _serviceComputeApiVersion = options.ServiceComputeApiVersion; + _serviceKeyVaultApiVersion = options.ServiceKeyVaultApiVersion; + _serviceStorageApiVersion = options.ServiceStorageApiVersion; + } + + public global::System.ClientModel.Primitives.ClientPipeline Pipeline { get; } + + public virtual global::Sample.KeyVault.KeyVault GetKeyVaultClient() + { + return (global::System.Threading.Volatile.Read(ref _cachedKeyVault) ?? (global::System.Threading.Interlocked.CompareExchange(ref _cachedKeyVault, new global::Sample.KeyVault.KeyVault(Pipeline, _endpoint, _serviceKeyVaultApiVersion, _subscriptionId), null) ?? _cachedKeyVault)); + } + + public virtual global::Sample.Storage.Storage GetStorageClient() + { + return (global::System.Threading.Volatile.Read(ref _cachedStorage) ?? (global::System.Threading.Interlocked.CompareExchange(ref _cachedStorage, new global::Sample.Storage.Storage(Pipeline, _endpoint, _serviceStorageApiVersion, _subscriptionId), null) ?? _cachedStorage)); + } + + public virtual global::Sample.Compute.Compute GetComputeClient() + { + return (global::System.Threading.Volatile.Read(ref _cachedCompute) ?? (global::System.Threading.Interlocked.CompareExchange(ref _cachedCompute, new global::Sample.Compute.Compute(Pipeline, _endpoint, _serviceComputeApiVersion, _subscriptionId), null) ?? _cachedCompute)); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/MultiServiceClient_GeneratesExpectedClientOptions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/MultiServiceClient_GeneratesExpectedClientOptions.cs new file mode 100644 index 00000000000..f3f3227ef90 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/MultiServiceClient_GeneratesExpectedClientOptions.cs @@ -0,0 +1,47 @@ +// + +#nullable disable + +using System; +using System.ClientModel.Primitives; + +namespace Sample +{ + public partial class TestClientOptions : global::System.ClientModel.Primitives.ClientPipelineOptions + { + private const global::Sample.TestClientOptions.ServiceAVersion LatestServiceAVersion = global::Sample.TestClientOptions.ServiceAVersion.V2_0; + private const global::Sample.TestClientOptions.ServiceBVersion LatestServiceBVersion = global::Sample.TestClientOptions.ServiceBVersion.V4_0; + + public TestClientOptions(global::Sample.TestClientOptions.ServiceAVersion serviceAVersion = LatestServiceAVersion, global::Sample.TestClientOptions.ServiceBVersion serviceBVersion = LatestServiceBVersion) + { + ServiceAApiVersion = serviceAVersion switch + { + global::Sample.TestClientOptions.ServiceAVersion.V1_0 => "1.0", + global::Sample.TestClientOptions.ServiceAVersion.V2_0 => "2.0", + _ => throw new global::System.NotSupportedException() + }; + ServiceBApiVersion = serviceBVersion switch + { + global::Sample.TestClientOptions.ServiceBVersion.V3_0 => "3.0", + global::Sample.TestClientOptions.ServiceBVersion.V4_0 => "4.0", + _ => throw new global::System.NotSupportedException() + }; + } + + internal string ServiceAApiVersion { get; } + + internal string ServiceBApiVersion { get; } + + public enum ServiceAVersion + { + V1_0 = 1, + V2_0 = 2 + } + + public enum ServiceBVersion + { + V3_0 = 1, + V4_0 = 2 + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/MultiServiceClient_WithThreeServices_GeneratesExpectedClientOptions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/MultiServiceClient_WithThreeServices_GeneratesExpectedClientOptions.cs new file mode 100644 index 00000000000..d7c4856dae2 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ClientOptionsProviderTests/MultiServiceClient_WithThreeServices_GeneratesExpectedClientOptions.cs @@ -0,0 +1,64 @@ +// + +#nullable disable + +using System; +using System.ClientModel.Primitives; + +namespace Sample +{ + public partial class TestClientOptions : global::System.ClientModel.Primitives.ClientPipelineOptions + { + private const global::Sample.TestClientOptions.ServiceComputeVersion LatestServiceComputeVersion = global::Sample.TestClientOptions.ServiceComputeVersion.V2024_07_01; + private const global::Sample.TestClientOptions.ServiceKeyVaultVersion LatestServiceKeyVaultVersion = global::Sample.TestClientOptions.ServiceKeyVaultVersion.V7_5; + private const global::Sample.TestClientOptions.ServiceStorageVersion LatestServiceStorageVersion = global::Sample.TestClientOptions.ServiceStorageVersion.V2024_01_01; + + public TestClientOptions(global::Sample.TestClientOptions.ServiceKeyVaultVersion serviceKeyVaultVersion = LatestServiceKeyVaultVersion, global::Sample.TestClientOptions.ServiceStorageVersion serviceStorageVersion = LatestServiceStorageVersion, global::Sample.TestClientOptions.ServiceComputeVersion serviceComputeVersion = LatestServiceComputeVersion) + { + ServiceKeyVaultApiVersion = serviceKeyVaultVersion switch + { + global::Sample.TestClientOptions.ServiceKeyVaultVersion.V7_4 => "7.4", + global::Sample.TestClientOptions.ServiceKeyVaultVersion.V7_5 => "7.5", + _ => throw new global::System.NotSupportedException() + }; + ServiceStorageApiVersion = serviceStorageVersion switch + { + global::Sample.TestClientOptions.ServiceStorageVersion.V2023_01_01 => "2023-01-01", + global::Sample.TestClientOptions.ServiceStorageVersion.V2024_01_01 => "2024-01-01", + _ => throw new global::System.NotSupportedException() + }; + ServiceComputeApiVersion = serviceComputeVersion switch + { + global::Sample.TestClientOptions.ServiceComputeVersion.V2023_07_01 => "2023-07-01", + global::Sample.TestClientOptions.ServiceComputeVersion.V2024_03_01 => "2024-03-01", + global::Sample.TestClientOptions.ServiceComputeVersion.V2024_07_01 => "2024-07-01", + _ => throw new global::System.NotSupportedException() + }; + } + + internal string ServiceComputeApiVersion { get; } + + internal string ServiceKeyVaultApiVersion { get; } + + internal string ServiceStorageApiVersion { get; } + + public enum ServiceComputeVersion + { + V2023_07_01 = 1, + V2024_03_01 = 2, + V2024_07_01 = 3 + } + + public enum ServiceKeyVaultVersion + { + V7_4 = 1, + V7_5 = 2 + } + + public enum ServiceStorageVersion + { + V2023_01_01 = 1, + V2024_01_01 = 2 + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputLibrary.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputLibrary.cs index 66d953f5c6b..d49942c1e39 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputLibrary.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputLibrary.cs @@ -42,6 +42,8 @@ internal InputNamespace Load() private bool? _hasMultipartFormDataOperation; public bool HasMultipartFormDataOperation => _hasMultipartFormDataOperation ??= GetHasMultipartFormDataOperation(); + private bool? _hasMultiServiceClient; + public bool HasMultiServiceClient => _hasMultiServiceClient ??= GetHasMultiServiceClient(); private bool GetHasMultipartFormDataOperation() { @@ -58,5 +60,18 @@ private bool GetHasMultipartFormDataOperation() return false; } + + private bool GetHasMultiServiceClient() + { + foreach (var client in InputNamespace.Clients) + { + if (client.IsMultiServiceClient) + { + return true; + } + } + + return false; + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputClient.cs index ca001c8a192..c56e34bc65a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputClient.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/InputTypes/InputClient.cs @@ -30,6 +30,7 @@ public InputClient( Doc = doc; Methods = methods; Parameters = parameters; + IsMultiServiceClient = isMultiServiceClient; Parent = parent; Children = children ?? []; ApiVersions = apiVersions ?? []; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/PropertyWireInformation.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/PropertyWireInformation.cs index 336f4d3968f..68b43a1c015 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/PropertyWireInformation.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/PropertyWireInformation.cs @@ -14,8 +14,9 @@ public class PropertyWireInformation : WireInformation public bool IsNullable { get; } public bool IsDiscriminator { get; } public bool IsHttpMetadata { get; } + public bool IsApiVersion { get; } internal FormattableString? Description { get; } - public PropertyWireInformation(SerializationFormat serializationFormat, bool isRequired, bool isReadOnly, bool isNullable, bool isDiscriminator, string serializedName, bool isHttpMetadata) + public PropertyWireInformation(SerializationFormat serializationFormat, bool isRequired, bool isReadOnly, bool isNullable, bool isDiscriminator, string serializedName, bool isHttpMetadata, bool isApiVersion) : base(serializationFormat, serializedName) { IsRequired = isRequired; @@ -23,6 +24,7 @@ public PropertyWireInformation(SerializationFormat serializationFormat, bool isR IsNullable = isNullable; IsDiscriminator = isDiscriminator; IsHttpMetadata = isHttpMetadata; + IsApiVersion = isApiVersion; } /// @@ -40,6 +42,7 @@ internal PropertyWireInformation(InputProperty inputProperty) IsNullable = inputProperty.Type is InputNullableType; IsDiscriminator = modelProperty != null && modelProperty.IsDiscriminator; Description = DocHelpers.GetFormattableDescription(inputProperty.Summary, inputProperty.Doc); + IsApiVersion = inputProperty.IsApiVersion; } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ApiVersionEnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ApiVersionEnumProvider.cs index e5262535ae0..76f41c1e6f4 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ApiVersionEnumProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ApiVersionEnumProvider.cs @@ -8,6 +8,7 @@ using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Shared; using Microsoft.TypeSpec.Generator.Utilities; using static Microsoft.TypeSpec.Generator.Snippets.Snippet; @@ -15,12 +16,35 @@ namespace Microsoft.TypeSpec.Generator.Providers { internal sealed class ApiVersionEnumProvider : FixedEnumProvider { - private const string ApiVersionEnumName = "ServiceVersion"; + private const string ServicePrefix = "Service"; + private const string VersionSuffix = "Version"; + private const string ApiVersionEnumName = $"{ServicePrefix}{VersionSuffix}"; private const string ApiVersionEnumDescription = "The version of the service to use."; - public ApiVersionEnumProvider(InputEnumType input, TypeProvider? declaringType) : base(input, declaringType) { } + private readonly InputEnumType _inputEnum; + + public ApiVersionEnumProvider(InputEnumType input, TypeProvider? declaringType) : base(input, declaringType) + { + _inputEnum = input; + } + + protected override string BuildName() + { + List apiVersionEnums = [.. CodeModelGenerator.Instance.InputLibrary.InputNamespace.Enums + .Where(e => e.Usage.HasFlag(InputModelTypeUsage.ApiVersionEnum))]; + + if (CodeModelGenerator.Instance.InputLibrary.HasMultiServiceClient && apiVersionEnums.Count > 1) + { + var serviceNamespace = _inputEnum.Namespace; + if (!string.IsNullOrEmpty(serviceNamespace)) + { + return ClientHelper.BuildNameForService(serviceNamespace, ServicePrefix, VersionSuffix); + } + } + + return ApiVersionEnumName; + } - protected override string BuildName() => ApiVersionEnumName; protected override FormattableString BuildDescription() => $"{ApiVersionEnumDescription}"; protected override IReadOnlyList BuildEnumValues() diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CanonicalTypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CanonicalTypeProvider.cs index 0e5cd1d05c6..754dadefabc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CanonicalTypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CanonicalTypeProvider.cs @@ -120,6 +120,7 @@ protected internal override PropertyProvider[] BuildProperties() customProperty.Type.IsNullable, false, serializedName ?? customProperty.Name.ToVariableName(), + false, false); } else @@ -237,6 +238,7 @@ protected internal override FieldProvider[] BuildFields() customField.Type.IsNullable, false, serializedName ?? customField.Name.ToVariableName(), + false, false); } else diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/EnumProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/EnumProvider.cs index cf63a97c190..027fa93e408 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/EnumProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/EnumProvider.cs @@ -32,11 +32,14 @@ protected EnumProvider(InputEnumType? input) _inputType = input; _deprecated = input?.Deprecation; IsExtensible = input?.IsExtensible ?? false; + InputNamespace = input?.Namespace; } internal EnumProvider? FixedEnumView { get; set; } internal EnumProvider? ExtensibleEnumView { get; set; } + public string? InputNamespace { get; } + public bool IsExtensible { get; } private bool? _isIntValue; internal bool IsIntValueType => _isIntValue ??= EnumUnderlyingType.Equals(typeof(int)) || EnumUnderlyingType.Equals(typeof(long)); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/ClientHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/ClientHelper.cs new file mode 100644 index 00000000000..527959086ec --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/ClientHelper.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.TypeSpec.Generator.Shared +{ + internal static class ClientHelper + { + /// + /// Builds a name with the specified prefix and suffix, ensuring no duplicate prefix or suffix + /// if the namespace/service segment already contains them. + /// + /// The full service name. + /// The prefix to ensure (e.g., "Service", "Latest"). + /// The suffix to ensure (e.g., "Version"). + /// A name with the specified prefix and suffix. + public static string BuildNameForService(string serviceName, string prefix, string suffix) + { + var lastNamespaceSegment = serviceName.AsSpan(); + int lastDotIndex = serviceName.LastIndexOf('.'); + if (lastDotIndex >= 0) + { + lastNamespaceSegment = lastNamespaceSegment.Slice(lastDotIndex + 1); + } + + bool hasPrefix = lastNamespaceSegment.StartsWith(prefix.AsSpan(), StringComparison.OrdinalIgnoreCase); + bool hasSuffix = lastNamespaceSegment.EndsWith(suffix.AsSpan(), StringComparison.OrdinalIgnoreCase); + + return (hasPrefix, hasSuffix) switch + { + (true, true) => lastNamespaceSegment.ToString(), + (true, false) => $"{lastNamespaceSegment}{suffix}", + (false, true) => $"{prefix}{lastNamespaceSegment}", + (false, false) => $"{prefix}{lastNamespaceSegment}{suffix}" + }; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/CanonicalTypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/CanonicalTypeProviderTests.cs index e78b83151b7..c754dbd6d00 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/CanonicalTypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/CanonicalTypeProviderTests.cs @@ -282,9 +282,9 @@ protected internal override PropertyProvider[] BuildProperties() return [ // customized by the NamedSymbol - new PropertyProvider($"Int property", MethodSignatureModifiers.Public, typeof(int), "IntProperty", new AutoPropertyBody(true), this, wireInfo: new PropertyWireInformation(SerializationFormat.Default, true, true, true, false, "intProperty", false)), + new PropertyProvider($"Int property", MethodSignatureModifiers.Public, typeof(int), "IntProperty", new AutoPropertyBody(true), this, wireInfo: new PropertyWireInformation(SerializationFormat.Default, true, true, true, false, "intProperty", false, false)), // not customized by the NamedSymbol - new PropertyProvider($"Spec property", MethodSignatureModifiers.Public, typeof(string), "SpecProperty", new AutoPropertyBody(false), this, wireInfo: new PropertyWireInformation(SerializationFormat.Default, true, true, true, false, "specProperty", false)), + new PropertyProvider($"Spec property", MethodSignatureModifiers.Public, typeof(string), "SpecProperty", new AutoPropertyBody(false), this, wireInfo: new PropertyWireInformation(SerializationFormat.Default, true, true, true, false, "specProperty", false, false)), // customized by the NamedSymbol with null wire info new PropertyProvider($"Null Wire Info property", MethodSignatureModifiers.Public, typeof(string), "NullWireInfoProperty", new AutoPropertyBody(false), this, wireInfo: new PropertyWireInformation(nullInputWireInfo)) ]; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/ApiVersionEnumProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/ApiVersionEnumProviderTests.cs index 1f0d3d094dd..548e71f06a2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/ApiVersionEnumProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/EnumProviders/ApiVersionEnumProviderTests.cs @@ -143,5 +143,73 @@ public async Task CustomEnumMembers() Assert.AreEqual(new LiteralExpression(2), provider.EnumValues[2].Field.InitializationValue); Assert.AreEqual(apiVersions[2], provider.EnumValues[2].Value); } + + [Test] + public void MultiServiceClient_WithMultipleApiVersionEnums_GeneratesCorrectEnumNames() + { + // Setup multiservice client with multiple API version enums from different services + var keyVaultEnum = InputFactory.StringEnum( + "KeyVaultVersion", + [("7.4", "7.4"), ("7.5", "7.5")], + usage: InputModelTypeUsage.ApiVersionEnum, + clientNamespace: "Sample.KeyVault"); + var storageEnum = InputFactory.StringEnum( + "StorageVersion", + [("2023-01-01", "2023-01-01"), ("2024-01-01", "2024-01-01")], + usage: InputModelTypeUsage.ApiVersionEnum, + clientNamespace: "Sample.Storage"); + + var client = InputFactory.Client("TestClient", isMultiServiceClient: true); + + MockHelpers.LoadMockGenerator( + inputEnumTypes: [keyVaultEnum, storageEnum], + inputClients: [client]); + + // Create enum providers for each API version enum + var mockDeclaringType = new Mock(); + mockDeclaringType.Protected().Setup("BuildName").Returns("TestClientOptions"); + mockDeclaringType.Protected().Setup("BuildNamespace").Returns("Sample"); + + var keyVaultEnumType = EnumProvider.Create(keyVaultEnum, mockDeclaringType.Object); + Assert.IsTrue(keyVaultEnumType is ApiVersionEnumProvider); + var keyVaultProvider = (ApiVersionEnumProvider)keyVaultEnumType; + + var storageEnumType = EnumProvider.Create(storageEnum, mockDeclaringType.Object); + Assert.IsTrue(storageEnumType is ApiVersionEnumProvider); + var storageProvider = (ApiVersionEnumProvider)storageEnumType; + + // Verify enum names follow the multiservice naming pattern: Service{ServiceName}Version + Assert.AreEqual("ServiceKeyVaultVersion", keyVaultProvider.Name); + Assert.AreEqual("ServiceStorageVersion", storageProvider.Name); + } + + [Test] + public void MultiServiceClient_WithOneApiVersionEnums_GeneratesCorrectEnumNames() + { + // Setup multiservice client with multiple API version enums from different services + var keyVaultEnum = InputFactory.StringEnum( + "KeyVaultVersion", + [("7.4", "7.4"), ("7.5", "7.5")], + usage: InputModelTypeUsage.ApiVersionEnum, + clientNamespace: "Sample.KeyVault"); + + var client = InputFactory.Client("TestClient", isMultiServiceClient: true); + + MockHelpers.LoadMockGenerator( + inputEnumTypes: [keyVaultEnum], + inputClients: [client]); + + // Create enum providers for each API version enum + var mockDeclaringType = new Mock(); + mockDeclaringType.Protected().Setup("BuildName").Returns("TestClientOptions"); + mockDeclaringType.Protected().Setup("BuildNamespace").Returns("Sample"); + + var keyVaultEnumType = EnumProvider.Create(keyVaultEnum, mockDeclaringType.Object); + Assert.IsTrue(keyVaultEnumType is ApiVersionEnumProvider); + var keyVaultProvider = (ApiVersionEnumProvider)keyVaultEnumType; + + // Verify enum names follow the multiservice naming pattern: Service{ServiceName}Version + Assert.AreEqual("ServiceVersion", keyVaultProvider.Name); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/FieldProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/FieldProviderTests.cs index c2c63b340d6..bebea0badd3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/FieldProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/FieldProviderTests.cs @@ -28,7 +28,7 @@ public void AsParameterRespectsChangesToFieldType() { var field = new FieldProvider(FieldModifiers.Private, new CSharpType(typeof(int)), "name", new TestTypeProvider()); field.Type = new CSharpType(typeof(string)); - field.WireInfo = new PropertyWireInformation(SerializationFormat.Default, true, true, true, false, "newName", false); + field.WireInfo = new PropertyWireInformation(SerializationFormat.Default, true, true, true, false, "newName", false, false); var parameter = field.AsParameter; Assert.IsTrue(parameter.Type.Equals(typeof(string))); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Shared/ClientHelperTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Shared/ClientHelperTests.cs new file mode 100644 index 00000000000..e9ea467e259 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Shared/ClientHelperTests.cs @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.TypeSpec.Generator.Shared; +using NUnit.Framework; + +namespace Microsoft.TypeSpec.Generator.Tests.Shared +{ + public class ClientHelperTests + { + [Test] + public void BuildNameForService_NoPrefixNoSuffix_AddsBoth() + { + var result = ClientHelper.BuildNameForService("Sample.KeyVault", "Service", "Version"); + Assert.AreEqual("ServiceKeyVaultVersion", result); + } + + [Test] + public void BuildNameForService_HasPrefixNoSuffix_AddsSuffix() + { + var result = ClientHelper.BuildNameForService("Sample.ServiceKeyVault", "Service", "Version"); + Assert.AreEqual("ServiceKeyVaultVersion", result); + } + + [Test] + public void BuildNameForService_NoPrefixHasSuffix_AddsPrefix() + { + var result = ClientHelper.BuildNameForService("Sample.KeyVaultVersion", "Service", "Version"); + Assert.AreEqual("ServiceKeyVaultVersion", result); + } + + [Test] + public void BuildNameForService_HasPrefixAndSuffix_ReturnsAsIs() + { + var result = ClientHelper.BuildNameForService("Sample.ServiceKeyVaultVersion", "Service", "Version"); + Assert.AreEqual("ServiceKeyVaultVersion", result); + } + + // Namespace handling tests + + [Test] + public void BuildNameForService_MultipleNamespaceSegments_ExtractsLastSegment() + { + var result = ClientHelper.BuildNameForService("Azure.ResourceManager.Storage", "Service", "Version"); + Assert.AreEqual("ServiceStorageVersion", result); + } + + [Test] + public void BuildNameForService_NoNamespaceSegments_UsesFullName() + { + var result = ClientHelper.BuildNameForService("Storage", "Service", "Version"); + Assert.AreEqual("ServiceStorageVersion", result); + } + + [Test] + public void BuildNameForService_SingleDotNamespace_ExtractsLastSegment() + { + var result = ClientHelper.BuildNameForService("Sample.Compute", "Service", "Version"); + Assert.AreEqual("ServiceComputeVersion", result); + } + + // Case insensitivity tests + + [Test] + public void BuildNameForService_PrefixCaseInsensitive_LowerCase() + { + var result = ClientHelper.BuildNameForService("Sample.serviceKeyVault", "Service", "Version"); + Assert.AreEqual("serviceKeyVaultVersion", result); + } + + [Test] + public void BuildNameForService_SuffixCaseInsensitive_LowerCase() + { + var result = ClientHelper.BuildNameForService("Sample.KeyVaultversion", "Service", "Version"); + Assert.AreEqual("ServiceKeyVaultversion", result); + } + + [Test] + public void BuildNameForService_BothCaseInsensitive_MixedCase() + { + var result = ClientHelper.BuildNameForService("Sample.SERVICEKeyVaultVERSION", "Service", "Version"); + Assert.AreEqual("SERVICEKeyVaultVERSION", result); + } + + // Edge cases + + [Test] + public void BuildNameForService_EmptyServiceName_ReturnsEmptyWithPrefixAndSuffix() + { + var result = ClientHelper.BuildNameForService("", "Service", "Version"); + Assert.AreEqual("ServiceVersion", result); + } + + [Test] + public void BuildNameForService_TrailingDot_ReturnsEmptyWithPrefixAndSuffix() + { + var result = ClientHelper.BuildNameForService("Sample.", "Service", "Version"); + Assert.AreEqual("ServiceVersion", result); + } + + [Test] + public void BuildNameForService_EmptyPrefix_OnlyAddsSuffix() + { + var result = ClientHelper.BuildNameForService("Sample.KeyVault", "", "Version"); + Assert.AreEqual("KeyVaultVersion", result); + } + + [Test] + public void BuildNameForService_EmptySuffix_OnlyAddsPrefix() + { + var result = ClientHelper.BuildNameForService("Sample.KeyVault", "Service", ""); + Assert.AreEqual("ServiceKeyVault", result); + } + + [Test] + public void BuildNameForService_BothPrefixAndSuffixEmpty_ReturnsLastSegment() + { + var result = ClientHelper.BuildNameForService("Sample.KeyVault", "", ""); + Assert.AreEqual("KeyVault", result); + } + + [Test] + public void BuildNameForService_ServiceNameEqualsPrefix_AddsSuffix() + { + var result = ClientHelper.BuildNameForService("Sample.Service", "Service", "Version"); + Assert.AreEqual("ServiceVersion", result); + } + + [Test] + public void BuildNameForService_ServiceNameEqualsSuffix_AddsPrefix() + { + var result = ClientHelper.BuildNameForService("Sample.Version", "Service", "Version"); + Assert.AreEqual("ServiceVersion", result); + } + + [Test] + public void BuildNameForService_ServiceNameEqualsPrefixAndSuffix_ReturnsAsIs() + { + var result = ClientHelper.BuildNameForService("Sample.ServiceVersion", "Service", "Version"); + Assert.AreEqual("ServiceVersion", result); + } + + [Test] + public void BuildNameForService_AzureKeyVault_GeneratesCorrectName() + { + var result = ClientHelper.BuildNameForService("Azure.Security.KeyVault", "Service", "Version"); + Assert.AreEqual("ServiceKeyVaultVersion", result); + } + + [Test] + public void BuildNameForService_AzureStorage_GeneratesCorrectName() + { + var result = ClientHelper.BuildNameForService("Azure.Storage.Blobs", "Service", "Version"); + Assert.AreEqual("ServiceBlobsVersion", result); + } + + [Test] + public void BuildNameForService_ApiVersionSuffix_GeneratesCorrectName() + { + var result = ClientHelper.BuildNameForService("Sample.KeyVault", "Service", "ApiVersion"); + Assert.AreEqual("ServiceKeyVaultApiVersion", result); + } + + [Test] + public void BuildNameForService_LatestPrefix_GeneratesCorrectName() + { + var result = ClientHelper.BuildNameForService("Sample.KeyVault", "Latest", "Version"); + Assert.AreEqual("LatestKeyVaultVersion", result); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Shared/MethodSignatureHelperTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Shared/MethodSignatureHelperTests.cs index 2de0a7de9b8..c07b3d131ce 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Shared/MethodSignatureHelperTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Shared/MethodSignatureHelperTests.cs @@ -9,7 +9,7 @@ using NUnit.Framework; using static Microsoft.TypeSpec.Generator.Snippets.Snippet; -namespace Microsoft.TypeSpec.Generator.Tests +namespace Microsoft.TypeSpec.Generator.Tests.Shared { public class MethodSignatureHelperTests { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TestHelpers/MockHelpers.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TestHelpers/MockHelpers.cs index f4dc7467d95..246ae07e3a2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TestHelpers/MockHelpers.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TestHelpers/MockHelpers.cs @@ -33,6 +33,7 @@ public async static Task> LoadMockGeneratorAsync( InputModelType[]? inputModelTypes = null, InputEnumType[]? inputEnumTypes = null, InputLiteralType[]? inputLiteralTypes = null, + InputClient[]? inputClients = null, Func>? compilation = null, Func>? lastContractCompilation = null, IEnumerable? additionalMetadataReferences = null, @@ -51,6 +52,7 @@ public async static Task> LoadMockGeneratorAsync( inputModelTypes, inputEnumTypes, inputLiteralTypes, + inputClients, additionalMetadataReferences, sharedSourceDirectories, typesToKeep, @@ -76,6 +78,7 @@ public static Mock LoadMockGenerator( InputModelType[]? inputModelTypes = null, InputEnumType[]? inputEnumTypes = null, InputLiteralType[]? inputLiteralTypes = null, + InputClient[]? inputClients = null, IEnumerable? additionalMetadataReferences = null, IEnumerable? sharedSourceDirectories = null, IEnumerable? typesToKeep = null, @@ -122,6 +125,7 @@ public static Mock LoadMockGenerator( inputNamespaceName ?? "Sample", models: inputModelTypes, enums: inputEnumTypes, + clients: inputClients, constants: inputLiteralTypes)); mockGenerator.Setup(p => p.InputLibrary).Returns(mockInputLibrary.Object);