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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Kiota.Builder/Configuration/GenerationConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ public bool NoWorkspace
get; set;
}

public HashSet<string> Overlays { get; set; } = new(0, StringComparer.OrdinalIgnoreCase);

public int MaxDegreeOfParallelism { get; set; } = -1;
public object Clone()
{
Expand Down Expand Up @@ -184,6 +186,7 @@ public object Clone()
DisableSSLValidation = DisableSSLValidation,
ExportPublicApi = ExportPublicApi,
PluginAuthInformation = PluginAuthInformation,
Overlays = Overlays
Copy link

Copilot AI Jul 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clone method assigns the same Overlays instance rather than copying it. Change to Overlays = Overlays.ToHashSet(StringComparer.OrdinalIgnoreCase) to avoid shared mutable state.

Suggested change
Overlays = Overlays
Overlays = Overlays?.ToHashSet(StringComparer.OrdinalIgnoreCase)

Copilot uses AI. Check for mistakes.
};
}
private static readonly StringIEnumerableDeepComparer comparer = new();
Expand Down
1 change: 1 addition & 0 deletions src/Kiota.Builder/Kiota.Builder.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AsyncKeyedLock" Version="7.1.6" />
<PackageReference Include="BinkyLabs.OpenApi.Overlays" Version="1.0.0-preview.4" />
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Kiota.Bundle" Version="1.19.0" />
Expand Down
7 changes: 7 additions & 0 deletions src/Kiota.Builder/Lock/KiotaLock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ public bool DisableSSLValidation
/// The OpenAPI validation rules to disable during the generation.
/// </summary>
public HashSet<string> DisabledValidationRules { get; set; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// The overlays used for this client.
/// </summary>
public HashSet<string> Overlays { get; set; } = new(StringComparer.OrdinalIgnoreCase);

#pragma warning restore CA2227
/// <summary>
/// Updates the passed configuration with the values from the lock file.
Expand All @@ -123,6 +128,7 @@ public void UpdateGenerationConfigurationFromLock(GenerationConfiguration config
config.OpenAPIFilePath = DescriptionLocation;
config.DisabledValidationRules = DisabledValidationRules.ToHashSet(StringComparer.OrdinalIgnoreCase);
config.DisableSSLValidation = DisableSSLValidation;
config.Overlays = Overlays;
}
/// <summary>
/// Initializes a new instance of the <see cref="KiotaLock"/> class.
Expand Down Expand Up @@ -152,5 +158,6 @@ public KiotaLock(GenerationConfiguration config)
DescriptionLocation = config.OpenAPIFilePath;
DisabledValidationRules = config.DisabledValidationRules.ToHashSet(StringComparer.OrdinalIgnoreCase);
DisableSSLValidation = config.DisableSSLValidation;
Overlays = config.Overlays;
}
}
85 changes: 82 additions & 3 deletions src/Kiota.Builder/OpenApiDocumentDownloadService.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection.Metadata;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
using BinkyLabs.OpenApi.Overlays;
using BinkyLabs.OpenApi.Overlays.Reader;
using Kiota.Builder.Caching;
using Kiota.Builder.Configuration;
using Kiota.Builder.Extensions;
Expand Down Expand Up @@ -96,7 +100,7 @@ ex is SecurityException ||
return (input, isDescriptionFromWorkspaceCopy);
}

internal async Task<ReadResult?> GetDocumentWithResultFromStreamAsync(Stream input, GenerationConfiguration config, bool generating = false, CancellationToken cancellationToken = default)
internal async Task<Microsoft.OpenApi.Reader.ReadResult?> GetDocumentWithResultFromStreamAsync(Stream input, GenerationConfiguration config, bool generating = false, CancellationToken cancellationToken = default)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
Expand All @@ -122,9 +126,9 @@ ex is SecurityException ||
if (addPluginsExtensions)
settings.AddPluginsExtensions();// Add all extensions for plugins

var rawUri = config.OpenAPIFilePath.TrimEnd(KiotaBuilder.ForwardSlash);
try
{
var rawUri = config.OpenAPIFilePath.TrimEnd(KiotaBuilder.ForwardSlash);
var lastSlashIndex = rawUri.LastIndexOf(KiotaBuilder.ForwardSlash);
if (lastSlashIndex < 0)
lastSlashIndex = rawUri.Length - 1;
Expand All @@ -137,7 +141,82 @@ ex is SecurityException ||
{
// couldn't parse the URL, it's probably a local file
}
var readResult = await OpenApiDocument.LoadAsync(input, settings: settings, cancellationToken: cancellationToken).ConfigureAwait(false);

Microsoft.OpenApi.Reader.ReadResult readResult = new Microsoft.OpenApi.Reader.ReadResult() { };
if (config.Overlays.Count != 0)
{
// TODO : handle multiple Overlays
var overlay = config.Overlays.First();
Copy link

Copilot AI Jul 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Either implement support for multiple overlays or remove/update this TODO with a clearer next step to avoid stale comments.

Suggested change
// TODO : handle multiple Overlays
var overlay = config.Overlays.First();

Copilot uses AI. Check for mistakes.
var overlaysSettings = new OverlayReaderSettings
{
OpenApiSettings = settings
};

var cachingProvider = new DocumentCachingProvider(HttpClient, Logger)
{
ClearCache = config.ClearCache,
};

Uri? overlayUri = null;
if (Uri.TryCreate(overlay, UriKind.Absolute, out var absoluteUri))
{
overlayUri = absoluteUri;
}
else if (Uri.TryCreate(overlay, UriKind.Relative, out var relativeUri))
{
overlayUri = relativeUri;
}

if (overlayUri is null)
{
throw new InvalidOperationException($"The overlay '{overlay}' is not a valid URI.");
}

BinkyLabs.OpenApi.Overlays.ReadResult? readOverlayResult = null;

if (overlayUri.IsAbsoluteUri && overlayUri.Scheme is "http" or "https")
{
var fileName = overlay is string name && !string.IsNullOrEmpty(name) ? name : "overlay.yml";
var inputOverlay = await cachingProvider.GetDocumentAsync(overlayUri, "generation", fileName, cancellationToken: cancellationToken).ConfigureAwait(false);

readOverlayResult = await OverlayDocument.LoadFromStreamAsync(inputOverlay, null, overlaysSettings, cancellationToken).ConfigureAwait(false);
}
else
{
readOverlayResult = await OverlayDocument.LoadFromUrlAsync(overlay, overlaysSettings, cancellationToken).ConfigureAwait(false);

}

if (readOverlayResult is null)
{
throw new InvalidOperationException($"Could not read the overlay document at {overlayUri}. Please ensure the URI is valid and accessible.");
}

readResult.Diagnostic = new OpenApiDiagnostic()
{
Errors = readOverlayResult.Diagnostic?.Errors ?? [],
Warnings = readOverlayResult.Diagnostic?.Warnings ?? [],
};

if (readOverlayResult.Document is not null)
{
var (document, overlaysDiagnostics, documentDiagnostics) = await readOverlayResult.Document.ApplyToDocumentStreamAsync(input, settings.BaseUrl ?? new Uri("file://" + rawUri), null, overlaysSettings, cancellationToken: cancellationToken)
.ConfigureAwait(false);

readResult.Diagnostic.Errors.AddRange(documentDiagnostics?.Errors ?? []);
readResult.Diagnostic.Warnings.AddRange(documentDiagnostics?.Warnings ?? []);
readResult.Diagnostic.Errors.AddRange(overlaysDiagnostics?.Errors ?? []);
readResult.Diagnostic.Warnings.AddRange(overlaysDiagnostics?.Warnings ?? []);

readResult.Document = document;
}
}
else
{
readResult = await OpenApiDocument.LoadAsync(input, settings: settings, cancellationToken: cancellationToken).ConfigureAwait(false);
}


stopwatch.Stop();
if (generatingMode && readResult.Diagnostic?.Warnings is { Count: > 0 })
foreach (var warning in readResult.Diagnostic.Warnings)
Expand Down
13 changes: 13 additions & 0 deletions src/kiota/Handlers/Client/AddHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ public required Option<bool> SkipGenerationOption
{
get; init;
}
public required Option<List<string>> OverlaysOption
{
get; init;
}

public override async Task<int> InvokeAsync(InvocationContext context)
{
Expand All @@ -101,6 +105,7 @@ public override async Task<int> InvokeAsync(InvocationContext context)
List<string>? excludePatterns0 = context.ParseResult.GetValueForOption(ExcludePatternsOption);
List<string>? disabledValidationRules0 = context.ParseResult.GetValueForOption(DisabledValidationRulesOption);
List<string>? structuredMimeTypes0 = context.ParseResult.GetValueForOption(StructuredMimeTypesOption);
List<string>? overlays0 = context.ParseResult.GetValueForOption(OverlaysOption);
var logLevel = context.ParseResult.FindResultFor(LogLevelOption)?.GetValueOrDefault() as LogLevel?;
CancellationToken cancellationToken = context.BindingContext.GetService(typeof(CancellationToken)) is CancellationToken token ? token : CancellationToken.None;

Expand All @@ -125,13 +130,15 @@ public override async Task<int> InvokeAsync(InvocationContext context)
List<string> excludePatterns = excludePatterns0.OrEmpty();
List<string> disabledValidationRules = disabledValidationRules0.OrEmpty();
List<string> structuredMimeTypes = structuredMimeTypes0.OrEmpty();
List<string> overlays = overlays0.OrEmpty();
AssignIfNotNullOrEmpty(output, (c, s) => c.OutputPath = s);
AssignIfNotNullOrEmpty(openapi, (c, s) => c.OpenAPIFilePath = s);
AssignIfNotNullOrEmpty(className, (c, s) => c.ClientClassName = s);
AssignIfNotNullOrEmpty(namespaceName, (c, s) => c.ClientNamespaceName = s);
Configuration.Generation.UsesBackingStore = backingStore;
Configuration.Generation.ExcludeBackwardCompatible = excludeBackwardCompatible;
Configuration.Generation.IncludeAdditionalData = includeAdditionalData;

Configuration.Generation.Language = language;
WarnUsingPreviewLanguage(language);
Configuration.Generation.TypeAccessModifier = typeAccessModifier;
Expand All @@ -150,6 +157,12 @@ public override async Task<int> InvokeAsync(InvocationContext context)
Configuration.Generation.StructuredMimeTypes = new(structuredMimeTypes.SelectMany(static x => x.Split(' ', StringSplitOptions.RemoveEmptyEntries))
.Select(static x => x.TrimQuotes()));

if (overlays.Count != 0)
Configuration.Generation.Overlays = overlays
.Select(static x => x.TrimQuotes())
.SelectMany(static x => x.Split(',', StringSplitOptions.RemoveEmptyEntries))
.ToHashSet(StringComparer.OrdinalIgnoreCase);

Configuration.Generation.OpenAPIFilePath = GetAbsolutePath(Configuration.Generation.OpenAPIFilePath);
Configuration.Generation.OutputPath = NormalizeSlashesInPath(GetAbsolutePath(Configuration.Generation.OutputPath));
Configuration.Generation.ApiManifestPath = NormalizeSlashesInPath(GetAbsolutePath(Configuration.Generation.ApiManifestPath));
Expand Down
15 changes: 15 additions & 0 deletions src/kiota/Handlers/Client/EditHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ public required Option<bool> SkipGenerationOption
{
get; init;
}
public required Option<List<string>> OverlaysOption
{
get; init;
}

public override async Task<int> InvokeAsync(InvocationContext context)
{
Expand All @@ -101,6 +105,8 @@ public override async Task<int> InvokeAsync(InvocationContext context)
List<string>? excludePatterns = context.ParseResult.GetValueForOption(ExcludePatternsOption);
List<string>? disabledValidationRules = context.ParseResult.GetValueForOption(DisabledValidationRulesOption);
List<string>? structuredMimeTypes = context.ParseResult.GetValueForOption(StructuredMimeTypesOption);
List<string>? overlays = context.ParseResult.GetValueForOption(OverlaysOption);

var logLevel = context.ParseResult.FindResultFor(LogLevelOption)?.GetValueOrDefault() as LogLevel?;
CancellationToken cancellationToken = context.BindingContext.GetService(typeof(CancellationToken)) is CancellationToken token ? token : CancellationToken.None;

Expand Down Expand Up @@ -159,6 +165,7 @@ public override async Task<int> InvokeAsync(InvocationContext context)
Configuration.Generation.ExcludeBackwardCompatible = excludeBackwardCompatible.Value;
if (includeAdditionalData.HasValue)
Configuration.Generation.IncludeAdditionalData = includeAdditionalData.Value;

AssignIfNotNullOrEmpty(output, (c, s) => c.OutputPath = s);
AssignIfNotNullOrEmpty(openapi, (c, s) => c.OpenAPIFilePath = s);
AssignIfNotNullOrEmpty(className, (c, s) => c.ClientClassName = s);
Expand All @@ -176,6 +183,14 @@ public override async Task<int> InvokeAsync(InvocationContext context)
Configuration.Generation.StructuredMimeTypes = new(structuredMimeTypes.SelectMany(static x => x.Split(' ', StringSplitOptions.RemoveEmptyEntries))
.Select(static x => x.TrimQuotes()));

if (overlays is { Count: > 0 })
Configuration.Generation.Overlays = overlays.Select(static x => x.TrimQuotes())
.SelectMany(static x => x.Split(',', StringSplitOptions.RemoveEmptyEntries))
.ToHashSet(StringComparer.OrdinalIgnoreCase);




DefaultSerializersAndDeserializers(Configuration.Generation);
var builder = new KiotaBuilder(logger, Configuration.Generation, httpClient, true);
var result = await builder.GenerateClientAsync(cancellationToken).ConfigureAwait(false);
Expand Down
13 changes: 13 additions & 0 deletions src/kiota/Handlers/KiotaGenerateCommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public required Option<List<string>> StructuredMimeTypesOption
{
get; init;
}

public required Option<List<string>> OverlaysOption
{
get; init;
}
public override async Task<int> InvokeAsync(InvocationContext context)
{
// Span start time
Expand All @@ -97,6 +102,7 @@ public override async Task<int> InvokeAsync(InvocationContext context)
List<string>? includePatterns0 = context.ParseResult.GetValueForOption(IncludePatternsOption);
List<string>? excludePatterns0 = context.ParseResult.GetValueForOption(ExcludePatternsOption);
List<string>? disabledValidationRules0 = context.ParseResult.GetValueForOption(DisabledValidationRulesOption);
List<string>? overlays0 = context.ParseResult.GetValueForOption(OverlaysOption);
bool cleanOutput = context.ParseResult.GetValueForOption(CleanOutputOption);
List<string>? structuredMimeTypes0 = context.ParseResult.GetValueForOption(StructuredMimeTypesOption);
var logLevel = context.ParseResult.FindResultFor(LogLevelOption)?.GetValueOrDefault() as LogLevel?;
Expand All @@ -123,6 +129,7 @@ public override async Task<int> InvokeAsync(InvocationContext context)
List<string> excludePatterns = excludePatterns0.OrEmpty();
List<string> disabledValidationRules = disabledValidationRules0.OrEmpty();
List<string> structuredMimeTypes = structuredMimeTypes0.OrEmpty();
List<string> overlays = overlays0.OrEmpty();
AssignIfNotNullOrEmpty(output, (c, s) => c.OutputPath = s);
AssignIfNotNullOrEmpty(openapi, (c, s) => c.OpenAPIFilePath = s);
AssignIfNotNullOrEmpty(manifest, (c, s) => c.ApiManifestPath = s);
Expand Down Expand Up @@ -151,6 +158,12 @@ public override async Task<int> InvokeAsync(InvocationContext context)
Configuration.Generation.StructuredMimeTypes = new(structuredMimeTypes.SelectMany(static x => x.Split(' ', StringSplitOptions.RemoveEmptyEntries))
.Select(static x => x.TrimQuotes()));

if (overlays.Count != 0)
Configuration.Generation.Overlays = overlays
.Select(static x => x.TrimQuotes())
.SelectMany(static x => x.Split(',', StringSplitOptions.RemoveEmptyEntries))
.ToHashSet(StringComparer.OrdinalIgnoreCase);

Configuration.Generation.OpenAPIFilePath = GetAbsolutePath(Configuration.Generation.OpenAPIFilePath);
Configuration.Generation.OutputPath = NormalizeSlashesInPath(GetAbsolutePath(Configuration.Generation.OutputPath));
Configuration.Generation.ApiManifestPath = NormalizeSlashesInPath(GetAbsolutePath(Configuration.Generation.ApiManifestPath));
Expand Down
16 changes: 16 additions & 0 deletions src/kiota/Handlers/Plugin/AddHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ public required Option<bool> NoWorkspaceOption
{
get; init;
}
public required Option<List<string>> OverlaysOption
{
get; init;
}

public override async Task<int> InvokeAsync(InvocationContext context)
{
// Span start time
Expand All @@ -82,6 +87,8 @@ public override async Task<int> InvokeAsync(InvocationContext context)
string? className = context.ParseResult.GetValueForOption(ClassOption);
List<string>? includePatterns0 = context.ParseResult.GetValueForOption(IncludePatternsOption);
List<string>? excludePatterns0 = context.ParseResult.GetValueForOption(ExcludePatternsOption);
List<string>? overlays0 = context.ParseResult.GetValueForOption(OverlaysOption);

var logLevel = context.ParseResult.FindResultFor(LogLevelOption)?.GetValueOrDefault() as LogLevel?;
CancellationToken cancellationToken = context.BindingContext.GetService(typeof(CancellationToken)) is CancellationToken token ? token : CancellationToken.None;

Expand All @@ -108,6 +115,7 @@ public override async Task<int> InvokeAsync(InvocationContext context)
Configuration.Generation.SkipGeneration = skipGeneration;
Configuration.Generation.NoWorkspace = noWorkspace;
Configuration.Generation.Operation = ConsumerOperation.Add;

if (pluginTypes is { Count: > 0 })
Configuration.Generation.PluginTypes = pluginTypes.ToHashSet();
if (pluginAuthType.HasValue && !string.IsNullOrWhiteSpace(pluginAuthRefId))
Expand All @@ -116,6 +124,14 @@ public override async Task<int> InvokeAsync(InvocationContext context)
Configuration.Generation.IncludePatterns = includePatterns0.Select(static x => x.TrimQuotes()).ToHashSet(StringComparer.OrdinalIgnoreCase);
if (excludePatterns0 is { Count: > 0 })
Configuration.Generation.ExcludePatterns = excludePatterns0.Select(static x => x.TrimQuotes()).ToHashSet(StringComparer.OrdinalIgnoreCase);

if (overlays0 is { Count: > 0 })
Configuration.Generation.Overlays = overlays0
.Select(static x => x.TrimQuotes())
.SelectMany(static x => x.Split(',', StringSplitOptions.RemoveEmptyEntries))
.ToHashSet(StringComparer.OrdinalIgnoreCase);


Configuration.Generation.OpenAPIFilePath = GetAbsolutePath(Configuration.Generation.OpenAPIFilePath);
Configuration.Generation.OutputPath = NormalizeSlashesInPath(GetAbsolutePath(Configuration.Generation.OutputPath));
var (loggerFactory, logger) = GetLoggerAndFactory<KiotaBuilder>(context, Configuration.Generation.OutputPath);
Expand Down
Loading