From 741b473600451ca44500775f7f7200f661fe92f8 Mon Sep 17 00:00:00 2001 From: "Konstantin S." Date: Thu, 9 Apr 2026 08:38:02 +0400 Subject: [PATCH 1/2] feat(generator): support scoped OpenAPI servers --- .../Operations/CSharpEndPointFactory.cs | 14 +- .../CSharpOperationContextFactory.cs | 4 + .../Operations/CSharpServerFactory.cs | 126 ++++++++++++++ .../Operations/OpenApiOperationExtensions.cs | 86 +++++++++ .../AutoSDK.CSharp/Pipeline/CSharpPipeline.cs | 4 + src/libs/AutoSDK.CSharp/Pipeline/Data.cs | 144 ++++++++++++--- .../AutoSDK.CSharp/Sources/Sources.Clients.cs | 162 ++++++++++++++++- .../AutoSDK.CSharp/Sources/Sources.Methods.cs | 14 +- .../Sources/Sources.ServerSelection.cs | 69 ++++++++ src/libs/AutoSDK.CSharp/Sources/Sources.cs | 9 + .../AutoSDK.SourceGenerators/SdkGenerator.cs | 7 + src/libs/AutoSDK/Models/Client.cs | 3 +- src/libs/AutoSDK/Models/EndPoint.cs | 5 +- src/libs/AutoSDK/Models/OperationContext.cs | 2 + src/libs/AutoSDK/Models/ServerOption.cs | 8 + .../AutoSDK.UnitTests/NamingTests.Methods.cs | 4 + .../AutoSDK.UnitTests/ServerSelectionTests.cs | 164 ++++++++++++++++++ 17 files changed, 794 insertions(+), 31 deletions(-) create mode 100644 src/libs/AutoSDK.CSharp/Operations/CSharpServerFactory.cs create mode 100644 src/libs/AutoSDK.CSharp/Sources/Sources.ServerSelection.cs create mode 100644 src/libs/AutoSDK/Models/ServerOption.cs create mode 100644 src/tests/AutoSDK.UnitTests/ServerSelectionTests.cs diff --git a/src/libs/AutoSDK.CSharp/Operations/CSharpEndPointFactory.cs b/src/libs/AutoSDK.CSharp/Operations/CSharpEndPointFactory.cs index ed921de89ad..5fb84341785 100644 --- a/src/libs/AutoSDK.CSharp/Operations/CSharpEndPointFactory.cs +++ b/src/libs/AutoSDK.CSharp/Operations/CSharpEndPointFactory.cs @@ -175,6 +175,10 @@ public static EndPoint CreateEndPoint( OpenApiExtensions.GetExtensionBooleanValue( operation.Operation.Extensions, "x-autosdk-response-wrapper"); + var servers = CSharpServerFactory.CreateServerOptions(operation.Servers); + var primaryServer = servers.Length > 0 + ? servers[0] + : default; return new EndPoint( Id: endPointId, @@ -187,7 +191,7 @@ public static EndPoint CreateEndPoint( FileNameWithoutExtension: $"{operation.Settings.Namespace}.{className}.{notAsyncMethodName}", InterfaceFileNameWithoutExtension: $"{operation.Settings.Namespace}.I{className}.{notAsyncMethodName}", Tag: operation.Tag, - BaseUrl: string.Empty, + BaseUrl: primaryServer.Url ?? string.Empty, StreamFormat: streamFormat, Path: preparedPath, RequestMediaType: requestMediaType, @@ -201,7 +205,9 @@ public static EndPoint CreateEndPoint( ContentType: successResponse.ContentType, Summary: operation.Operation.GetXmlDocumentationSummary(), Description: operation.Operation.Description ?? string.Empty, - BaseUrlSummary: string.Empty, + BaseUrlSummary: operation.Servers.Count > 0 + ? operation.Servers[0].Description?.ClearForXml() ?? string.Empty + : string.Empty, CliAction: (OpenApiExtensions.TryGetExtensionStringValue( operation.Operation.Extensions, "x-cli-action", out var cliActionStr) @@ -220,7 +226,9 @@ public static EndPoint CreateEndPoint( ? streamTerminator ?? "[DONE]" : string.Empty, Remarks: GetCodeSamplesRemarks(operation.Operation), - GenerateResponseWrapper: generateResponseWrapper); + GenerateResponseWrapper: generateResponseWrapper, + Servers: servers, + HasServerOverride: operation.HasServerOverride); } private static void DeduplicateMethodParameterNames(List parameters) diff --git a/src/libs/AutoSDK.CSharp/Operations/CSharpOperationContextFactory.cs b/src/libs/AutoSDK.CSharp/Operations/CSharpOperationContextFactory.cs index fd9c9915644..6d6baadc64f 100644 --- a/src/libs/AutoSDK.CSharp/Operations/CSharpOperationContextFactory.cs +++ b/src/libs/AutoSDK.CSharp/Operations/CSharpOperationContextFactory.cs @@ -14,6 +14,8 @@ public static OperationContext CreateOperationContext( string operationPath, System.Net.Http.HttpMethod operationType, IReadOnlyList? operationSchemas, + IList effectiveServers, + bool hasServerOverride, IList globalSecurityRequirements, IReadOnlyDictionary? resolvedTags = null) { @@ -45,6 +47,8 @@ public static OperationContext CreateOperationContext( var context = new OperationContext(settings, globalSettings, operation, operationPath, operationType) { Schemas = operationSchemas ?? (IReadOnlyCollection)[], + Servers = effectiveServers, + HasServerOverride = hasServerOverride, Tags = tags, GlobalSecurityRequirements = globalSecurityRequirements, Tag = GetOperationTag(operation, settings, firstTag, resolvedTags), diff --git a/src/libs/AutoSDK.CSharp/Operations/CSharpServerFactory.cs b/src/libs/AutoSDK.CSharp/Operations/CSharpServerFactory.cs new file mode 100644 index 00000000000..c31f40eb06a --- /dev/null +++ b/src/libs/AutoSDK.CSharp/Operations/CSharpServerFactory.cs @@ -0,0 +1,126 @@ +using System.Collections.Immutable; +using System.Text; +using AutoSDK.Extensions; +using AutoSDK.Models; +using Microsoft.OpenApi; + +namespace AutoSDK.Generation; + +internal static class CSharpServerFactory +{ + public static EquatableArray CreateServerOptions( + IList? servers) + { + if (servers == null || servers.Count == 0) + { + return []; + } + + var builder = ImmutableArray.CreateBuilder(servers.Count); + var seenUrls = new HashSet(StringComparer.Ordinal); + var seenIds = new HashSet(StringComparer.Ordinal); + + foreach (var server in servers) + { + if (server == null) + { + continue; + } + + var url = server.ExpandServerTemplate().Trim(); + if (string.IsNullOrWhiteSpace(url) || !seenUrls.Add(url)) + { + continue; + } + + var id = GetServerId(server, url); + if (!seenIds.Add(id)) + { + var suffix = 2; + var candidate = id; + while (!seenIds.Add(candidate)) + { + candidate = $"{id}-{suffix++}"; + } + + id = candidate; + } + + builder.Add(new ServerOption( + Id: id, + Name: GetServerName(server, url), + Url: url, + Description: server.Description ?? string.Empty)); + } + + return builder.ToImmutable(); + } + + private static string GetServerId( + OpenApiServer server, + string url) + { + if (OpenApiExtensions.TryGetExtensionStringValue(server.Extensions, "x-server-id", out var explicitId) && + !string.IsNullOrWhiteSpace(explicitId)) + { + return SanitizeToken(explicitId); + } + + return Uri.TryCreate(url, UriKind.Absolute, out var uri) + ? SanitizeToken($"{uri.Scheme}-{uri.Host}{uri.AbsolutePath}") + : SanitizeToken(url); + } + + private static string GetServerName( + OpenApiServer server, + string url) + { + if (OpenApiExtensions.TryGetExtensionStringValue(server.Extensions, "x-server-name", out var explicitName) && + !string.IsNullOrWhiteSpace(explicitName)) + { + return explicitName.Trim(); + } + + if (!string.IsNullOrWhiteSpace(server.Description)) + { + return (server.Description ?? string.Empty).Trim(); + } + + if (Uri.TryCreate(url, UriKind.Absolute, out var uri)) + { + var path = uri.AbsolutePath.Trim('/').Replace('/', ' '); + return string.IsNullOrWhiteSpace(path) + ? uri.Host + : $"{uri.Host} {path}"; + } + + return url; + } + + private static string SanitizeToken(string value) + { + value = value ?? throw new ArgumentNullException(nameof(value)); + + var builder = new StringBuilder(value.Length); + var previousWasDash = false; + + foreach (var c in value) + { + if (char.IsLetterOrDigit(c)) + { + builder.Append(char.ToLowerInvariant(c)); + previousWasDash = false; + } + else if (!previousWasDash) + { + builder.Append('-'); + previousWasDash = true; + } + } + + var token = builder.ToString().Trim('-'); + return string.IsNullOrWhiteSpace(token) + ? "server" + : token; + } +} diff --git a/src/libs/AutoSDK.CSharp/Operations/OpenApiOperationExtensions.cs b/src/libs/AutoSDK.CSharp/Operations/OpenApiOperationExtensions.cs index 0ddbbe2fd84..f6ac198f32f 100644 --- a/src/libs/AutoSDK.CSharp/Operations/OpenApiOperationExtensions.cs +++ b/src/libs/AutoSDK.CSharp/Operations/OpenApiOperationExtensions.cs @@ -25,6 +25,7 @@ public static IReadOnlyList GetOperations( var schemasByOperation = BuildSchemasByOperation(filteredSchemas); var globalSecurity = openApiDocument.Security ?? []; + var documentServers = openApiDocument.Servers ?? []; var results = new List(); foreach (var path in pathItems) { @@ -43,6 +44,15 @@ public static IReadOnlyList GetOperations( schemasByOperation.TryGetValue(op.Value, out var operationSchemas); + var effectiveServers = GetEffectiveServers( + documentServers, + path.Value.Servers, + op.Value.Servers); + var hasServerOverride = HasServerOverride( + documentServers, + path.Value.Servers, + op.Value.Servers); + results.Add(CSharpOperationContextFactory.CreateOperationContext( settings: settings, globalSettings: globalSettings, @@ -50,6 +60,8 @@ public static IReadOnlyList GetOperations( operationPath: path.Key, operationType: op.Key, operationSchemas: operationSchemas, + effectiveServers: effectiveServers, + hasServerOverride: hasServerOverride, globalSecurityRequirements: globalSecurity, resolvedTags: resolvedTags)); } @@ -76,6 +88,7 @@ public static IReadOnlyList GetWebhookOperations( var schemasByOperation = BuildSchemasByOperation(filteredSchemas); var globalSecurity = openApiDocument.Security ?? []; + var documentServers = openApiDocument.Servers ?? []; var results = new List(); foreach (var webhook in webhooks) @@ -95,6 +108,15 @@ public static IReadOnlyList GetWebhookOperations( schemasByOperation.TryGetValue(op.Value, out var operationSchemas); + var effectiveServers = GetEffectiveServers( + documentServers, + webhook.Value.Servers, + op.Value.Servers); + var hasServerOverride = HasServerOverride( + documentServers, + webhook.Value.Servers, + op.Value.Servers); + results.Add(CSharpOperationContextFactory.CreateOperationContext( settings: settings, globalSettings: globalSettings, @@ -102,6 +124,8 @@ public static IReadOnlyList GetWebhookOperations( operationPath: webhook.Key, operationType: op.Key, operationSchemas: operationSchemas, + effectiveServers: effectiveServers, + hasServerOverride: hasServerOverride, globalSecurityRequirements: globalSecurity, resolvedTags: resolvedTags)); } @@ -140,4 +164,66 @@ private static bool ShouldIgnoreOperation( return settings.UseExtensionNaming && OpenApiExtensions.ShouldIgnoreOperationForDotNet(operation.Extensions); } + + private static IList GetEffectiveServers( + IList documentServers, + IList? pathServers, + IList? operationServers) + { + if (operationServers is { Count: > 0 }) + { + return operationServers; + } + + if (pathServers is { Count: > 0 }) + { + return pathServers; + } + + return documentServers; + } + + private static bool HasServerOverride( + IList documentServers, + IList? pathServers, + IList? operationServers) + { + if (operationServers is { Count: > 0 }) + { + var inheritedServers = pathServers is { Count: > 0 } + ? pathServers + : documentServers; + return !AreEquivalent(operationServers, inheritedServers); + } + + return pathServers is { Count: > 0 } && + !AreEquivalent(pathServers, documentServers); + } + + private static bool AreEquivalent( + IList left, + IList right) + { + if (ReferenceEquals(left, right)) + { + return true; + } + + if (left.Count != right.Count) + { + return false; + } + + for (var i = 0; i < left.Count; i++) + { + var leftUrl = left[i].ExpandServerTemplate(); + var rightUrl = right[i].ExpandServerTemplate(); + if (!string.Equals(leftUrl, rightUrl, StringComparison.Ordinal)) + { + return false; + } + } + + return true; + } } diff --git a/src/libs/AutoSDK.CSharp/Pipeline/CSharpPipeline.cs b/src/libs/AutoSDK.CSharp/Pipeline/CSharpPipeline.cs index 4f193244e4e..cbf09a738e1 100644 --- a/src/libs/AutoSDK.CSharp/Pipeline/CSharpPipeline.cs +++ b/src/libs/AutoSDK.CSharp/Pipeline/CSharpPipeline.cs @@ -154,6 +154,10 @@ public static IReadOnlyList GenerateFiles( .Concat([Sources.Polyfills(settings, cancellationToken)]) .Concat([Sources.Exceptions(settings, cancellationToken)]) .Concat([Sources.PathBuilder(settings, cancellationToken)]) + .Concat(data.Clients.Any(static x => x.UsesServerSelectionSupport) || + data.Methods.Any(static x => x.ClientUsesServerSelectionSupport) + ? [Sources.ServerSelectionSupport(settings, cancellationToken)] + : []) .Concat([Sources.OptionsSupport(settings, cancellationToken)]) .Concat(!data.Authorizations.IsEmpty ? [Sources.SecuritySupport(settings, cancellationToken)] diff --git a/src/libs/AutoSDK.CSharp/Pipeline/Data.cs b/src/libs/AutoSDK.CSharp/Pipeline/Data.cs index 3811d369a2f..8b3ef5c6f04 100644 --- a/src/libs/AutoSDK.CSharp/Pipeline/Data.cs +++ b/src/libs/AutoSDK.CSharp/Pipeline/Data.cs @@ -530,7 +530,6 @@ bool CanExpandReference(SchemaContext resolvedReference) .Values .ToArray(); var hasOAuth2Support = authorizations.Any(static x => x.Type is SecuritySchemeType.OAuth2); - var hasMutualTlsSupport = authorizations.Any(static x => x.Type is SecuritySchemeType.MutualTLS); var convertersBuilder = ImmutableArray.CreateBuilder(); // Enum converters @@ -607,12 +606,19 @@ bool CanExpandReference(SchemaContext resolvedReference) .Select(tag => resolvedIncludedTagsMap[tag.Name!]) .OrderBy(tag => tag.SafeName, StringComparer.Ordinal) .ToArray(); + var rootClassName = settings.ClassName.Replace(".", string.Empty); + var documentServers = CSharpServerFactory.CreateServerOptions(openApiDocument.Servers); + var clientServersByClass = BuildClientServerMap(methods, rootClassName, documentServers); + var usesServerSelectionSupport = clientServersByClass.Values.Any(static servers => servers.Length > 1); + methods = ApplyClientServerSelectionSupport(methods, clientServersByClass); + var rootClientServers = GetClientServers(rootClassName, clientServersByClass, documentServers); + Client[] clients = settings.GenerateSdk || settings.GenerateConstructors ? [new Client( Id: "MainConstructor", - ClassName: settings.ClassName.Replace(".", string.Empty), - FileNameWithoutExtension: $"{settings.Namespace}.{settings.ClassName.Replace(".", string.Empty)}", - InterfaceFileNameWithoutExtension: $"{settings.Namespace}.I{settings.ClassName.Replace(".", string.Empty)}", - BaseUrl: openApiDocument.Servers!.FirstOrDefault().ExpandServerTemplate(openApiDocument.Self), + ClassName: rootClassName, + FileNameWithoutExtension: $"{settings.Namespace}.{rootClassName}", + InterfaceFileNameWithoutExtension: $"{settings.Namespace}.I{rootClassName}", + BaseUrl: rootClientServers.FirstOrDefault().Url ?? string.Empty, Clients: settings.GroupByTags && (settings.GenerateSdk || settings.GenerateConstructors) ? [ .. resolvedIncludedTags.Select(tag => (PropertyData.Default with @@ -628,12 +634,13 @@ .. resolvedIncludedTags.Select(tag => (PropertyData.Default with ] : [], Summary: openApiDocument.Info?.Description?.ClearForXml() ?? string.Empty, - BaseUrlSummary: CreateServerSummary(openApiDocument.Servers!.FirstOrDefault()), + BaseUrlSummary: rootClientServers.FirstOrDefault().Description?.ClearForXml() ?? string.Empty, Settings: csharpSettings, GlobalSettings: csharpGlobalSettings, Converters: converters, HasOAuth2Support: hasOAuth2Support, - HasMutualTlsSupport: hasMutualTlsSupport)] : []; + Servers: rootClientServers, + UsesServerSelectionSupport: usesServerSelectionSupport)] : []; if (settings.GroupByTags && (settings.GenerateSdk || settings.GenerateConstructors)) { clients = clients.Concat( @@ -643,15 +650,16 @@ .. resolvedIncludedTags.Select(tag => (PropertyData.Default with ClassName: CSharpClientNameGenerator.Generate(tag), FileNameWithoutExtension: $"{settings.Namespace}.{CSharpClientNameGenerator.Generate(tag)}", InterfaceFileNameWithoutExtension: $"{settings.Namespace}.I{CSharpClientNameGenerator.Generate(tag)}", - BaseUrl: openApiDocument.Servers!.FirstOrDefault().ExpandServerTemplate(openApiDocument.Self), + BaseUrl: GetClientServers(CSharpClientNameGenerator.Generate(tag), clientServersByClass, documentServers).FirstOrDefault().Url ?? string.Empty, Clients: [], - Summary: tag.DocumentationSummary.ClearForXml(), - BaseUrlSummary: CreateServerSummary(openApiDocument.Servers!.FirstOrDefault()), + Summary: (!string.IsNullOrWhiteSpace(tag.DisplayName) ? tag.DisplayName : tag.Description)?.ClearForXml() ?? string.Empty, + BaseUrlSummary: GetClientServers(CSharpClientNameGenerator.Generate(tag), clientServersByClass, documentServers).FirstOrDefault().Description?.ClearForXml() ?? string.Empty, Settings: csharpSettings, GlobalSettings: csharpGlobalSettings, Converters: [], HasOAuth2Support: hasOAuth2Support, - HasMutualTlsSupport: hasMutualTlsSupport))) + Servers: GetClientServers(CSharpClientNameGenerator.Generate(tag), clientServersByClass, documentServers), + UsesServerSelectionSupport: usesServerSelectionSupport))) .ToArray(); } @@ -714,7 +722,8 @@ .. resolvedIncludedTags.Select(tag => (PropertyData.Default with BaseUrlSummary: string.Empty, Settings: csharpSettings, GlobalSettings: csharpGlobalSettings, - Converters: converters), + Converters: converters, + UsesServerSelectionSupport: usesServerSelectionSupport), Schemas: schemas, FilteredSchemas: filteredSchemas, Times: new Times( @@ -900,7 +909,6 @@ internal static Models.Data Enrich( .Values .ToArray(); var hasOAuth2Support = authorizations.Any(static x => x.Type is SecuritySchemeType.OAuth2); - var hasMutualTlsSupport = authorizations.Any(static x => x.Type is SecuritySchemeType.MutualTLS); var convertersBuilder = ImmutableArray.CreateBuilder(); foreach (var value in enums) @@ -974,14 +982,20 @@ internal static Models.Data Enrich( .Select(tag => resolvedIncludedTagsMap[tag.Name!]) .OrderBy(tag => tag.SafeName, StringComparer.Ordinal) .ToArray(); + var rootClassName = settings.ClassName.Replace(".", string.Empty); + var documentServers = CSharpServerFactory.CreateServerOptions(openApiDocument.Servers); + var clientServersByClass = BuildClientServerMap(methods, rootClassName, documentServers); + var usesServerSelectionSupport = clientServersByClass.Values.Any(static servers => servers.Length > 1); + methods = ApplyClientServerSelectionSupport(methods, clientServersByClass); + var rootClientServers = GetClientServers(rootClassName, clientServersByClass, documentServers); Client[] clients = settings.GenerateSdk || settings.GenerateConstructors ? [new Client( Id: "MainConstructor", - ClassName: settings.ClassName.Replace(".", string.Empty), - FileNameWithoutExtension: $"{settings.Namespace}.{settings.ClassName.Replace(".", string.Empty)}", - InterfaceFileNameWithoutExtension: $"{settings.Namespace}.I{settings.ClassName.Replace(".", string.Empty)}", - BaseUrl: openApiDocument.Servers!.FirstOrDefault().ExpandServerTemplate(openApiDocument.Self), + ClassName: rootClassName, + FileNameWithoutExtension: $"{settings.Namespace}.{rootClassName}", + InterfaceFileNameWithoutExtension: $"{settings.Namespace}.I{rootClassName}", + BaseUrl: rootClientServers.FirstOrDefault().Url ?? string.Empty, Clients: settings.GroupByTags && (settings.GenerateSdk || settings.GenerateConstructors) ? [ .. resolvedIncludedTags.Select(tag => (PropertyData.Default with @@ -997,12 +1011,13 @@ .. resolvedIncludedTags.Select(tag => (PropertyData.Default with ] : [], Summary: openApiDocument.Info?.Description?.ClearForXml() ?? string.Empty, - BaseUrlSummary: CreateServerSummary(openApiDocument.Servers!.FirstOrDefault()), + BaseUrlSummary: rootClientServers.FirstOrDefault().Description?.ClearForXml() ?? string.Empty, Settings: settings, GlobalSettings: globalSettings, Converters: converters, HasOAuth2Support: hasOAuth2Support, - HasMutualTlsSupport: hasMutualTlsSupport)] + Servers: rootClientServers, + UsesServerSelectionSupport: usesServerSelectionSupport)] : []; if (settings.GroupByTags && (settings.GenerateSdk || settings.GenerateConstructors)) @@ -1013,15 +1028,16 @@ .. resolvedIncludedTags.Select(tag => (PropertyData.Default with ClassName: CSharpClientNameGenerator.Generate(tag), FileNameWithoutExtension: $"{settings.Namespace}.{CSharpClientNameGenerator.Generate(tag)}", InterfaceFileNameWithoutExtension: $"{settings.Namespace}.I{CSharpClientNameGenerator.Generate(tag)}", - BaseUrl: openApiDocument.Servers!.FirstOrDefault().ExpandServerTemplate(openApiDocument.Self), + BaseUrl: GetClientServers(CSharpClientNameGenerator.Generate(tag), clientServersByClass, documentServers).FirstOrDefault().Url ?? string.Empty, Clients: [], - Summary: tag.DocumentationSummary.ClearForXml(), - BaseUrlSummary: CreateServerSummary(openApiDocument.Servers!.FirstOrDefault()), + Summary: (!string.IsNullOrWhiteSpace(tag.DisplayName) ? tag.DisplayName : tag.Description)?.ClearForXml() ?? string.Empty, + BaseUrlSummary: GetClientServers(CSharpClientNameGenerator.Generate(tag), clientServersByClass, documentServers).FirstOrDefault().Description?.ClearForXml() ?? string.Empty, Settings: settings, GlobalSettings: globalSettings, Converters: [], HasOAuth2Support: hasOAuth2Support, - HasMutualTlsSupport: hasMutualTlsSupport))) + Servers: GetClientServers(CSharpClientNameGenerator.Generate(tag), clientServersByClass, documentServers), + UsesServerSelectionSupport: usesServerSelectionSupport))) .ToArray(); } @@ -1083,7 +1099,8 @@ .. resolvedIncludedTags.Select(tag => (PropertyData.Default with BaseUrlSummary: string.Empty, Settings: settings, GlobalSettings: globalSettings, - Converters: converters), + Converters: converters, + UsesServerSelectionSupport: usesServerSelectionSupport), Schemas: schemas, FilteredSchemas: filteredSchemas, Times: new Times( @@ -1109,6 +1126,85 @@ .. resolvedIncludedTags.Select(tag => (PropertyData.Default with )); } + private static Dictionary> BuildClientServerMap( + IReadOnlyList methods, + string rootClassName, + EquatableArray documentServers) + { + var serversByClass = new Dictionary>(StringComparer.Ordinal); + + if (!documentServers.IsEmpty) + { + AddServers(rootClassName, documentServers); + } + + foreach (var method in methods) + { + var effectiveServers = method.Servers.IsEmpty + ? documentServers + : method.Servers; + if (effectiveServers.IsEmpty) + { + continue; + } + + AddServers(rootClassName, effectiveServers); + AddServers(method.ClassName, effectiveServers); + } + + return serversByClass.ToDictionary( + static pair => pair.Key, + static pair => pair.Value.ToImmutableArray().AsEquatableArray(), + StringComparer.Ordinal); + + void AddServers( + string className, + EquatableArray servers) + { + if (!serversByClass.TryGetValue(className, out var list)) + { + list = []; + serversByClass[className] = list; + } + + foreach (var server in servers) + { + if (list.Any(existing => + string.Equals(existing.Id, server.Id, StringComparison.Ordinal) || + string.Equals(existing.Url, server.Url, StringComparison.Ordinal))) + { + continue; + } + + list.Add(server); + } + } + } + + private static ImmutableArray ApplyClientServerSelectionSupport( + IReadOnlyList methods, + Dictionary> clientServersByClass) + { + return methods + .Select(method => method with + { + ClientUsesServerSelectionSupport = + clientServersByClass.TryGetValue(method.ClassName, out var servers) && + servers.Length > 1, + }) + .ToImmutableArray(); + } + + private static EquatableArray GetClientServers( + string className, + Dictionary> clientServersByClass, + EquatableArray documentServers) + { + return clientServersByClass.TryGetValue(className, out var servers) && !servers.IsEmpty + ? servers + : documentServers; + } + private static IEnumerable CreateEndPoints(OperationContext operation) { var fernStreaming = FernStreamingMetadata.TryCreate(operation); diff --git a/src/libs/AutoSDK.CSharp/Sources/Sources.Clients.cs b/src/libs/AutoSDK.CSharp/Sources/Sources.Clients.cs index d42c2bf3296..2bc94669921 100644 --- a/src/libs/AutoSDK.CSharp/Sources/Sources.Clients.cs +++ b/src/libs/AutoSDK.CSharp/Sources/Sources.Clients.cs @@ -12,6 +12,7 @@ public static string GenerateClient( var serializer = client.Settings.JsonSerializerType.GetSerializer(); var hasOptions = !client.Settings.HasJsonSerializerContext(); var rootClassName = client.Settings.ClassName.Replace(".", string.Empty); + var hasServerSelection = client.Servers.Length > 1; var suppressDeprecatedWarningsForJsonSerializerOptions = hasOptions && client.Settings.UsesSystemTextJson() && @@ -37,7 +38,7 @@ public sealed partial class {client.ClassName} : global::{client.Settings.Namesp public global::System.Net.Http.HttpClient HttpClient {{ get; }} /// - public System.Uri? BaseUri => HttpClient.BaseAddress; + public System.Uri? BaseUri => {(hasServerSelection ? "ResolveDisplayedBaseUri()" : "HttpClient.BaseAddress")}; /// public global::System.Collections.Generic.List Authorizations {{ get; }} @@ -53,6 +54,9 @@ public sealed partial class {client.ClassName} : global::{client.Settings.Namesp {(client.HasOAuth2Support ? $@" internal global::{client.Settings.Namespace}.{rootClassName}.AutoSDKOAuth2Coordinator AutoSDKOAuth2State {{ get; set; }} = new global::{client.Settings.Namespace}.{rootClassName}.AutoSDKOAuth2Coordinator();" : TrimmedLine)} +{(client.UsesServerSelectionSupport ? $@" + + internal global::{client.Settings.Namespace}.AutoSDKServerConfiguration AutoSDKServerConfiguration {{ get; set; }} = new global::{client.Settings.Namespace}.AutoSDKServerConfiguration();" : TrimmedLine)} {string.Empty.ToXmlDocumentationSummary(level: 8)} {(hasOptions ? $@" @@ -73,8 +77,28 @@ public sealed partial class {client.ClassName} : global::{client.Settings.Namesp ? "JsonSerializerOptions = JsonSerializerOptions," : "JsonSerializerContext = JsonSerializerContext,")} {(client.HasOAuth2Support ? "AutoSDKOAuth2State = AutoSDKOAuth2State," : TrimmedLine)} + {(client.UsesServerSelectionSupport ? "AutoSDKServerConfiguration = AutoSDKServerConfiguration," : TrimmedLine)} }}; ").Inject() : TrimmedLine)} +{(hasServerSelection ? $@" + + private static readonly global::{client.Settings.Namespace}.AutoSDKServer[] s_availableServers = new global::{client.Settings.Namespace}.AutoSDKServer[] + {{{GenerateServerDeclarations(client.Servers, client.Settings.Namespace, 12)} + }}; + + /// + /// The server options available for this client. + /// + public global::System.Collections.Generic.IReadOnlyList AvailableServers => s_availableServers; + + /// + /// The currently selected server for this client, if any. + /// + public global::{client.Settings.Namespace}.AutoSDKServer? SelectedServer + {{ + get => ResolveSelectedServer(); + set => SelectServer(value); + }}" : TrimmedLine)} /// /// Creates a new instance of the {client.ClassName}. @@ -131,6 +155,8 @@ public sealed partial class {client.ClassName} : global::{client.Settings.Namesp Authorizations = authorizations ?? new global::System.Collections.Generic.List(); Options = options ?? new global::{client.Settings.Namespace}.AutoSDKClientOptions(); _disposeHttpClient = disposeHttpClient; +{(client.UsesServerSelectionSupport ? @" + AutoSDKServerConfiguration.ExplicitBaseUri = baseUri ?? httpClient?.BaseAddress;" : TrimmedLine)} Initialized(HttpClient); }} @@ -158,6 +184,118 @@ partial void ProcessResponseContent( global::System.Net.Http.HttpClient client, global::System.Net.Http.HttpResponseMessage response, ref string content); +{(hasServerSelection ? $@" + + /// + /// Selects one of the generated server options by id. + /// + public bool TrySelectServer(string serverId) + {{ + if (string.IsNullOrWhiteSpace(serverId)) + {{ + return false; + }} + + foreach (var server in s_availableServers) + {{ + if (string.Equals(server.Id, serverId, global::System.StringComparison.OrdinalIgnoreCase)) + {{ + AutoSDKServerConfiguration.SelectedServer = server; + AutoSDKServerConfiguration.ExplicitBaseUri = null; + return true; + }} + }} + + return false; + }} + + /// + /// Clears the currently selected server. + /// + public void ClearSelectedServer() + {{ + AutoSDKServerConfiguration.SelectedServer = null; + }} + + private global::{client.Settings.Namespace}.AutoSDKServer? ResolveSelectedServer() + {{ + var selectedServer = AutoSDKServerConfiguration.SelectedServer; + if (selectedServer is null) + {{ + return null; + }} + + foreach (var server in s_availableServers) + {{ + if (string.Equals(server.Id, selectedServer.Id, global::System.StringComparison.Ordinal)) + {{ + return server; + }} + }} + + return null; + }} + + private void SelectServer(global::{client.Settings.Namespace}.AutoSDKServer? server) + {{ + if (server is null) + {{ + AutoSDKServerConfiguration.SelectedServer = null; + return; + }} + + foreach (var candidate in s_availableServers) + {{ + if (string.Equals(candidate.Id, server.Id, global::System.StringComparison.Ordinal)) + {{ + AutoSDKServerConfiguration.SelectedServer = candidate; + AutoSDKServerConfiguration.ExplicitBaseUri = null; + return; + }} + }} + + throw new global::System.ArgumentException(""The provided server is not available for this client."", nameof(server)); + }} + + private global::System.Uri? ResolveDisplayedBaseUri() + {{ + if (AutoSDKServerConfiguration.ExplicitBaseUri is global::System.Uri explicitBaseUri) + {{ + return explicitBaseUri; + }} + + return ResolveSelectedServer()?.Uri ?? HttpClient.BaseAddress; + }} + + private global::System.Uri? ResolveBaseUri( + global::{client.Settings.Namespace}.AutoSDKServer[] servers, + string defaultBaseUrl) + {{ + if (AutoSDKServerConfiguration.ExplicitBaseUri is global::System.Uri explicitBaseUri) + {{ + return explicitBaseUri; + }} + + if (AutoSDKServerConfiguration.SelectedServer is global::{client.Settings.Namespace}.AutoSDKServer selectedServer) + {{ + foreach (var server in servers) + {{ + if (string.Equals(server.Id, selectedServer.Id, global::System.StringComparison.Ordinal)) + {{ + return server.Uri; + }} + }} + }} + + if (servers.Length > 0) + {{ + return servers[0].Uri; + }} + + return string.IsNullOrWhiteSpace(defaultBaseUrl) + ? HttpClient.BaseAddress + : new global::System.Uri(defaultBaseUrl, global::System.UriKind.RelativeOrAbsolute); + }}" : TrimmedLine)} }} }}".RemoveBlankLinesWhereOnlyWhitespaces(); } @@ -168,6 +306,7 @@ public static string GenerateClientInterface( { var serializer = client.Settings.JsonSerializerType.GetSerializer(); var hasOptions = !client.Settings.HasJsonSerializerContext(); + var hasServerSelection = client.Servers.Length > 1; return $@" #nullable enable @@ -186,6 +325,27 @@ public partial interface I{client.ClassName} : global::System.IDisposable /// The base URL for the API. /// public System.Uri? BaseUri {{ get; }} +{(hasServerSelection ? $@" + + /// + /// The server options available for this client. + /// + public global::System.Collections.Generic.IReadOnlyList AvailableServers {{ get; }} + + /// + /// The currently selected server for this client, if any. + /// + public global::{client.Settings.Namespace}.AutoSDKServer? SelectedServer {{ get; set; }} + + /// + /// Selects one of the generated server options by id. + /// + public bool TrySelectServer(string serverId); + + /// + /// Clears the currently selected server. + /// + public void ClearSelectedServer();" : TrimmedLine)} /// /// The authorizations to use for the requests. diff --git a/src/libs/AutoSDK.CSharp/Sources/Sources.Methods.cs b/src/libs/AutoSDK.CSharp/Sources/Sources.Methods.cs index 2a8f41589a6..5045567520b 100644 --- a/src/libs/AutoSDK.CSharp/Sources/Sources.Methods.cs +++ b/src/libs/AutoSDK.CSharp/Sources/Sources.Methods.cs @@ -63,6 +63,10 @@ namespace {endPoint.Settings.Namespace} {{ public partial class {endPoint.ClassName} {{ +{(endPoint.ClientUsesServerSelectionSupport ? $@" + private static readonly global::{endPoint.Settings.Namespace}.AutoSDKServer[] s_{endPoint.NotAsyncMethodName}Servers = new global::{endPoint.Settings.Namespace}.AutoSDKServer[] + {{{GenerateServerDeclarations(endPoint.Servers, endPoint.Settings.Namespace, 12)} + }};" : TrimmedLine)} {(!endPoint.AuthorizationRequirements.IsEmpty ? GenerateSecurityRequirementsField(endPoint) : TrimmedLine)} partial void Prepare{endPoint.NotAsyncMethodName}Arguments( global::System.Net.Http.HttpClient httpClient{endPoint.Parameters @@ -1108,10 +1112,18 @@ public static string GeneratePathAndQuery( EndPoint endPoint, string authorizationVariableName = "Authorizations") { + var escapedBaseUrl = EscapeCSharpStringLiteral(endPoint.BaseUrl); + var baseUriExpression = endPoint.ClientUsesServerSelectionSupport + ? $@"ResolveBaseUri( + servers: s_{endPoint.NotAsyncMethodName}Servers, + defaultBaseUrl: ""{escapedBaseUrl}"")" + : endPoint.HasServerOverride && !string.IsNullOrWhiteSpace(endPoint.BaseUrl) + ? $@"HttpClient.BaseAddress ?? new global::System.Uri(""{escapedBaseUrl}"", global::System.UriKind.RelativeOrAbsolute)" + : "HttpClient.BaseAddress"; var code = @$" var __pathBuilder = new global::{endPoint.GlobalSettings.Namespace}.PathBuilder( path: {endPoint.Path}, - baseUri: HttpClient.BaseAddress);"; + baseUri: {baseUriExpression});"; if (endPoint.Authorizations.Any(x => x is { Type: SecuritySchemeType.ApiKey, In: ParameterLocation.Query })) { code += $@" diff --git a/src/libs/AutoSDK.CSharp/Sources/Sources.ServerSelection.cs b/src/libs/AutoSDK.CSharp/Sources/Sources.ServerSelection.cs new file mode 100644 index 00000000000..2c86e77be9c --- /dev/null +++ b/src/libs/AutoSDK.CSharp/Sources/Sources.ServerSelection.cs @@ -0,0 +1,69 @@ +using AutoSDK.Extensions; +using AutoSDK.Models; + +namespace AutoSDK.Generation; + +public static partial class Sources +{ + public static string GenerateServerSelectionSupport( + CSharpSettings settings) + { + return $@"#nullable enable + +namespace {settings.Namespace} +{{ + /// + /// Represents a concrete OpenAPI server option. + /// + public sealed class AutoSDKServer + {{ + public AutoSDKServer( + string id, + string name, + string url, + string description) + {{ + Id = id ?? throw new global::System.ArgumentNullException(nameof(id)); + Name = name ?? string.Empty; + Url = url ?? throw new global::System.ArgumentNullException(nameof(url)); + Description = description ?? string.Empty; + Uri = new global::System.Uri(url, global::System.UriKind.RelativeOrAbsolute); + }} + + public string Id {{ get; }} + public string Name {{ get; }} + public string Url {{ get; }} + public string Description {{ get; }} + public global::System.Uri Uri {{ get; }} + }} + + internal sealed class AutoSDKServerConfiguration + {{ + public global::System.Uri? ExplicitBaseUri {{ get; set; }} + public global::{settings.Namespace}.AutoSDKServer? SelectedServer {{ get; set; }} + }} +}}"; + } + + private static string GenerateServerDeclarations( + EquatableArray servers, + string @namespace, + int level) + { + return servers.Select(server => $@" +{new string(' ', level)}new global::{@namespace}.AutoSDKServer( +{new string(' ', level + 4)}id: ""{EscapeCSharpStringLiteral(server.Id)}"", +{new string(' ', level + 4)}name: ""{EscapeCSharpStringLiteral(server.Name)}"", +{new string(' ', level + 4)}url: ""{EscapeCSharpStringLiteral(server.Url)}"", +{new string(' ', level + 4)}description: ""{EscapeCSharpStringLiteral(server.Description)}""),").Inject(); + } + + private static string EscapeCSharpStringLiteral(string? value) + { + return (value ?? string.Empty) + .Replace("\\", "\\\\") + .Replace("\"", "\\\"") + .Replace("\r", "\\r") + .Replace("\n", "\\n"); + } +} diff --git a/src/libs/AutoSDK.CSharp/Sources/Sources.cs b/src/libs/AutoSDK.CSharp/Sources/Sources.cs index 80400e0fe05..52d02dd8759 100644 --- a/src/libs/AutoSDK.CSharp/Sources/Sources.cs +++ b/src/libs/AutoSDK.CSharp/Sources/Sources.cs @@ -407,6 +407,15 @@ public static FileWithName PathBuilder( Text: GeneratePathBuilder(settings, cancellationToken: cancellationToken)); } + public static FileWithName ServerSelectionSupport( + CSharpSettings settings, + CancellationToken cancellationToken = default) + { + return new FileWithName( + Name: $"{settings.Namespace}.ServerSelection.g.cs", + Text: GenerateServerSelectionSupport(settings)); + } + public static FileWithName OptionsSupport( CSharpSettings settings, CancellationToken cancellationToken = default) diff --git a/src/libs/AutoSDK.SourceGenerators/SdkGenerator.cs b/src/libs/AutoSDK.SourceGenerators/SdkGenerator.cs index 066c448c471..0a2cd5cb4ae 100644 --- a/src/libs/AutoSDK.SourceGenerators/SdkGenerator.cs +++ b/src/libs/AutoSDK.SourceGenerators/SdkGenerator.cs @@ -50,6 +50,13 @@ public void Initialize(IncrementalGeneratorInitializationContext context) : FileWithName.Empty .AsFileWithName(), context, Id) .AddSource(context); + data + .SelectAndReportExceptions((x, c) => x.Clients.Any(static y => y.UsesServerSelectionSupport) || + x.Methods.Any(static y => y.ClientUsesServerSelectionSupport) + ? Sources.ServerSelectionSupport(x.Converters.Settings, c) + : FileWithName.Empty + .AsFileWithName(), context, Id) + .AddSource(context); data .Collect() .SelectMany(static (x, _) => GetOptionsSupportSettings(x)) diff --git a/src/libs/AutoSDK/Models/Client.cs b/src/libs/AutoSDK/Models/Client.cs index 3ceb22b44ca..e1d8608a3f6 100644 --- a/src/libs/AutoSDK/Models/Client.cs +++ b/src/libs/AutoSDK/Models/Client.cs @@ -15,5 +15,6 @@ public record struct Client( Settings GlobalSettings, ImmutableArray Converters, bool HasOAuth2Support = false, - bool HasMutualTlsSupport = false + EquatableArray Servers = default, + bool UsesServerSelectionSupport = false ); diff --git a/src/libs/AutoSDK/Models/EndPoint.cs b/src/libs/AutoSDK/Models/EndPoint.cs index 101f59e1906..5dbef9f1341 100644 --- a/src/libs/AutoSDK/Models/EndPoint.cs +++ b/src/libs/AutoSDK/Models/EndPoint.cs @@ -37,7 +37,10 @@ public record struct EndPoint( bool? ForcedRequestStreamValue, string StreamTerminator, string Remarks, - bool GenerateResponseWrapper + bool GenerateResponseWrapper, + EquatableArray Servers = default, + bool HasServerOverride = false, + bool ClientUsesServerSelectionSupport = false ) { public bool Stream => StreamFormat != StreamFormat.None; diff --git a/src/libs/AutoSDK/Models/OperationContext.cs b/src/libs/AutoSDK/Models/OperationContext.cs index 3558978f59e..0c3e8980b01 100644 --- a/src/libs/AutoSDK/Models/OperationContext.cs +++ b/src/libs/AutoSDK/Models/OperationContext.cs @@ -17,6 +17,8 @@ public class OperationContext( public IReadOnlyCollection Schemas { get; set; } = []; public IList GlobalSecurityRequirements { get; set; } = []; + public IList Servers { get; set; } = []; + public bool HasServerOverride { get; set; } public HashSet Tags { get; set; } = []; public Tag Tag { get; set; } = Tag.Empty; diff --git a/src/libs/AutoSDK/Models/ServerOption.cs b/src/libs/AutoSDK/Models/ServerOption.cs new file mode 100644 index 00000000000..26012f624b0 --- /dev/null +++ b/src/libs/AutoSDK/Models/ServerOption.cs @@ -0,0 +1,8 @@ +namespace AutoSDK.Models; + +public record struct ServerOption( + string Id, + string Name, + string Url, + string Description +); diff --git a/src/tests/AutoSDK.UnitTests/NamingTests.Methods.cs b/src/tests/AutoSDK.UnitTests/NamingTests.Methods.cs index f28c451a4bd..86d0aba7574 100644 --- a/src/tests/AutoSDK.UnitTests/NamingTests.Methods.cs +++ b/src/tests/AutoSDK.UnitTests/NamingTests.Methods.cs @@ -33,6 +33,8 @@ public void SummaryMethodNames_RemoveApostrophesFromIdentifiers() operationPath: "/me/slack_id", operationType: System.Net.Http.HttpMethod.Get, operationSchemas: null, + effectiveServers: [], + hasServerOverride: false, globalSecurityRequirements: []); var endPoint = CSharpEndPointFactory.CreateEndPoint(context); @@ -65,6 +67,8 @@ public void SummaryMethodNames_StripExperimentalPrefixesFromIdentifiers() operationPath: "/sessions/{session_id}/insights", operationType: System.Net.Http.HttpMethod.Get, operationSchemas: null, + effectiveServers: [], + hasServerOverride: false, globalSecurityRequirements: []); var endPoint = CSharpEndPointFactory.CreateEndPoint(context); diff --git a/src/tests/AutoSDK.UnitTests/ServerSelectionTests.cs b/src/tests/AutoSDK.UnitTests/ServerSelectionTests.cs new file mode 100644 index 00000000000..7b96408ab26 --- /dev/null +++ b/src/tests/AutoSDK.UnitTests/ServerSelectionTests.cs @@ -0,0 +1,164 @@ +using AutoSDK.Generation; +using AutoSDK.Models; + +namespace AutoSDK.UnitTests; + +[TestClass] +public class ServerSelectionTests +{ + private static Settings DefaultSettings => Settings.Default with + { + Namespace = "G", + ClassName = "ServerClient", + GenerateModels = true, + }; + + [TestMethod] + public void Prepare_PathAndOperationServers_UsesCorrectPrecedence() + { + var data = PrepareData(""" +openapi: 3.0.3 +info: + title: Server Selection + version: 1.0.0 +servers: + - url: https://api.example.com/v1 + description: Public API +paths: + /users: + get: + operationId: listUsers + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + /admin/audits: + servers: + - url: https://admin.example.com/v2 + description: Admin API + get: + operationId: listAdminAudits + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + /admin/stats: + servers: + - url: https://admin.example.com/v2 + description: Admin API + get: + operationId: getAdminStats + servers: + - url: https://stats.example.com/v3 + description: Stats API + responses: + '200': + description: OK + content: + application/json: + schema: + type: object +"""); + + data.Clients.Should().ContainSingle(); + data.Clients[0].BaseUrl.Should().Be("https://api.example.com/v1"); + data.Clients[0].Servers.Select(x => x.Url).Should().BeEquivalentTo( + "https://api.example.com/v1", + "https://admin.example.com/v2", + "https://stats.example.com/v3"); + data.Clients[0].UsesServerSelectionSupport.Should().BeTrue(); + + data.Methods.Single(x => x.NotAsyncMethodName == "ListUsers").BaseUrl.Should().Be("https://api.example.com/v1"); + data.Methods.Single(x => x.NotAsyncMethodName == "ListAdminAudits").BaseUrl.Should().Be("https://admin.example.com/v2"); + data.Methods.Single(x => x.NotAsyncMethodName == "GetAdminStats").BaseUrl.Should().Be("https://stats.example.com/v3"); + data.Methods.Single(x => x.NotAsyncMethodName == "ListUsers").ClientUsesServerSelectionSupport.Should().BeTrue(); + data.Methods.Single(x => x.NotAsyncMethodName == "ListAdminAudits").HasServerOverride.Should().BeTrue(); + data.Methods.Single(x => x.NotAsyncMethodName == "GetAdminStats").HasServerOverride.Should().BeTrue(); + } + + [TestMethod] + public void GenerateClient_MultiServerSpec_EmitsSelectionApi() + { + var data = PrepareData(""" +openapi: 3.0.3 +info: + title: Multi Server + version: 1.0.0 +servers: + - url: https://api.example.com/v1 + description: Public API + - url: https://staging.example.com/v1 + description: Staging API +paths: + /users: + get: + operationId: listUsers + responses: + '200': + description: OK + content: + application/json: + schema: + type: object +"""); + + var clientCode = Sources.GenerateClient(data.Clients[0]); + var fileNames = CSharpPipeline.GenerateFiles(data).Select(x => x.Name).ToArray(); + + clientCode.Should().Contain("AvailableServers => s_availableServers"); + clientCode.Should().Contain("TrySelectServer(string serverId)"); + clientCode.Should().Contain("SelectedServer"); + clientCode.Should().Contain("https://staging.example.com/v1"); + fileNames.Should().Contain("G.ServerSelection.g.cs"); + } + + [TestMethod] + public void GenerateEndPoint_SingleScopedServer_UsesDirectFallbackWithoutSelectionSupport() + { + var data = PrepareData(""" +openapi: 3.0.3 +info: + title: Scoped Server + version: 1.0.0 +paths: + /admin: + servers: + - url: https://admin.example.com/v2 + description: Admin API + get: + operationId: getAdmin + responses: + '200': + description: OK + content: + application/json: + schema: + type: object +"""); + + data.Clients.Should().ContainSingle(); + data.Clients[0].UsesServerSelectionSupport.Should().BeFalse(); + + var endPoint = data.Methods.Single(); + var methodCode = Sources.GenerateEndPoint(endPoint); + var fileNames = CSharpPipeline.GenerateFiles(data).Select(x => x.Name).ToArray(); + + endPoint.HasServerOverride.Should().BeTrue(); + endPoint.ClientUsesServerSelectionSupport.Should().BeFalse(); + methodCode.Should().Contain("HttpClient.BaseAddress ?? new global::System.Uri(\"https://admin.example.com/v2\""); + methodCode.Should().NotContain("ResolveBaseUri("); + fileNames.Should().NotContain("G.ServerSelection.g.cs"); + } + + private static AutoSDK.Models.Data PrepareData(string yaml) + { + var settings = DefaultSettings; + return AutoSDK.Generation.Data.Prepare(((yaml, settings), GlobalSettings: settings)); + } +} From 4b3300d91def34c506d154a4376b7a11a065f8b1 Mon Sep 17 00:00:00 2001 From: "Konstantin S." Date: Fri, 10 Apr 2026 17:41:04 +0400 Subject: [PATCH 2/2] fix(generator): keep mutual tls client support --- src/libs/AutoSDK/Models/Client.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/AutoSDK/Models/Client.cs b/src/libs/AutoSDK/Models/Client.cs index e1d8608a3f6..da67b4703a2 100644 --- a/src/libs/AutoSDK/Models/Client.cs +++ b/src/libs/AutoSDK/Models/Client.cs @@ -15,6 +15,7 @@ public record struct Client( Settings GlobalSettings, ImmutableArray Converters, bool HasOAuth2Support = false, + bool HasMutualTlsSupport = false, EquatableArray Servers = default, bool UsesServerSelectionSupport = false );