Skip to content
Draft
Show file tree
Hide file tree
Changes from 8 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 = new (Overlays, StringComparer.OrdinalIgnoreCase),
};
}
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;
}
}
84 changes: 81 additions & 3 deletions src/Kiota.Builder/OpenApiDocumentDownloadService.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
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 +99,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 +125,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 +140,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