Skip to content

Add PowerPlatformSpecGeneratorPlugin and configuration schema #1184

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 28 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2602d56
Add PowerPlatformSpecGeneratorPlugin and configuration schema
May 15, 2025
3fc6cef
Merge branch 'dotnet:main' into PowerPlatformPlugin
rwilson504 Jun 20, 2025
1e21295
Add PowerPlatformSpecGeneratorPlugin for OpenAPI spec generation from…
Jun 20, 2025
3166f08
Refactor OpenApiSpecGeneratorPlugin and add PowerPlatformOpenApiSpecG…
Jun 23, 2025
fffe447
feat: Enhance OpenAPI Spec Generator Plugin with post-processing capa…
Jun 24, 2025
04ddcbe
fix: Update synchronous processing of path item details and improve p…
Jun 24, 2025
b15277b
feat: Enhance PowerPlatformOpenApiSpecGeneratorPlugin with detailed X…
Jun 24, 2025
4bdeece
fix: Improve logging for OpenAPI docs serialization in OpenApiSpecGen…
Jun 24, 2025
16dbc64
fix: Update comment in ProcessPathItem method to clarify extensibilit…
Jun 24, 2025
5430eaf
Merge branch 'main' into PowerPlatformPlugin
rwilson504 Jun 24, 2025
ad1edf7
fix: Rename $schema property to $schemaReference for clarity in schem…
Jun 24, 2025
e255206
Merge branch 'PowerPlatformPlugin' of https://github.com/rwilson504/d…
Jun 24, 2025
c31dff0
fix: Update ConnectorMetadataConfig to use IReadOnlyList for Categori…
Jun 24, 2025
5e5976d
feat: Introduce new configuration classes and prompts for OpenAPI spe…
Jun 24, 2025
2bc4dc9
fix: Refactor OpenAPI contact information handling to allow null valu…
Jun 24, 2025
1f37afd
fix: Update documentation to reflect the removal of the x-ms-generate…
Jun 24, 2025
3f3f387
fix: Refactor contact information handling in OpenAPI document to use…
Jun 24, 2025
57cc4af
fix: Update OpenApiSpecGeneratorPlugin and PowerPlatformOpenApiSpecGe…
Jun 24, 2025
ab7a267
fix: Update PowerPlatformOpenApiSpecGeneratorPlugin to use chat compl…
Jun 24, 2025
5f6ecad
fix: Update launch configuration to include config file argument and …
Jun 24, 2025
a964d37
fix: Update RemoveResponseHeadersIfDisabled method to handle null pat…
Jun 24, 2025
27037ea
fix: Rearrange properties in PowerPlatformOpenApiSpecGeneratorPlugin …
Jun 24, 2025
e0b64a4
fix: Add default values to properties in PowerPlatformOpenApiSpecGene…
Jun 24, 2025
7b5df25
fix: Refactor ProcessPathItemAsync method to improve async handling a…
Jun 24, 2025
032ae05
fix: Update GetOperationIdAsync and GetOperationDescriptionAsync meth…
Jun 24, 2025
264d676
fix: Remove unnecessary line from PowerPlatformOpenApiSpecGeneratorPl…
Jun 24, 2025
c2ce935
Refactor OpenApiSpecGeneratorPlugin and PowerPlatformOpenApiSpecGener…
Jun 26, 2025
9bd6b21
Merge branch 'main' into PowerPlatformPlugin
waldekmastykarz Jun 27, 2025
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
106 changes: 85 additions & 21 deletions DevProxy.Plugins/Generation/OpenApiSpecGeneratorPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,45 @@ public enum SpecFormat
Yaml
}

public class ContactConfig
{
public string Name { get; set; } = "Your Name";
public string Url { get; set; } = "https://www.yourwebsite.com";
public string Email { get; set; } = "your.email@yourdomain.com";
public OpenApiContact ToOpenApiContact()
{
return new OpenApiContact
{
Name = Name,
Url = !string.IsNullOrWhiteSpace(Url) ? new Uri(Url) : null,
Email = Email
};
}
}

public class ConnectorMetadataConfig
{
public string? Website { get; set; }
public string? PrivacyPolicy { get; set; }
private string[]? _categories;
public IReadOnlyList<string>? Categories
{
get => _categories;
set => _categories = value?.ToArray();
}
}

public sealed class OpenApiSpecGeneratorPluginConfiguration
{
public bool IncludeOptionsRequests { get; set; }
Copy link
Preview

Copilot AI Jun 24, 2025

Choose a reason for hiding this comment

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

The configuration class doesn't include properties for includeResponseHeaders, contact, and connectorMetadata, which are defined in the JSON schema. Add these properties so the plugin can bind the full configuration.

Copilot uses AI. Check for mistakes.

public SpecFormat SpecFormat { get; set; } = SpecFormat.Json;
public SpecVersion SpecVersion { get; set; } = SpecVersion.v3_0;
public ContactConfig Contact { get; set; } = new();
public ConnectorMetadataConfig ConnectorMetadata { get; set; } = new();
public bool IncludeResponseHeaders { get; set; }
}

public sealed class OpenApiSpecGeneratorPlugin(
public class OpenApiSpecGeneratorPlugin(
ILogger<OpenApiSpecGeneratorPlugin> logger,
ISet<UrlToWatch> urlsToWatch,
ILanguageModelClient languageModelClient,
Expand Down Expand Up @@ -92,9 +123,9 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e)
foreach (var request in e.RequestLogs)
{
if (request.MessageType != MessageType.InterceptedResponse ||
request.Context is null ||
request.Context.Session is null ||
!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, request.Context.Session.HttpClient.Request.RequestUri.AbsoluteUri))
request.Context is null ||
request.Context.Session is null ||
!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, request.Context.Session.HttpClient.Request.RequestUri.AbsoluteUri))
{
continue;
}
Expand All @@ -113,17 +144,7 @@ request.Context.Session is null ||
{
var pathItem = GetOpenApiPathItem(request.Context.Session);
var parametrizedPath = ParametrizePath(pathItem, request.Context.Session.HttpClient.Request.RequestUri);
var operationInfo = pathItem.Operations.First();
operationInfo.Value.OperationId = await GetOperationIdAsync(
operationInfo.Key.ToString(),
request.Context.Session.HttpClient.Request.RequestUri.GetLeftPart(UriPartial.Authority),
parametrizedPath
);
operationInfo.Value.Description = await GetOperationDescriptionAsync(
operationInfo.Key.ToString(),
request.Context.Session.HttpClient.Request.RequestUri.GetLeftPart(UriPartial.Authority),
parametrizedPath
);
await ProcessPathItemAsync(pathItem, request.Context.Session.HttpClient.Request.RequestUri, parametrizedPath);
AddOrMergePathItem(openApiDocs, pathItem, request.Context.Session.HttpClient.Request.RequestUri, parametrizedPath);
}
catch (Exception ex)
Expand All @@ -136,6 +157,9 @@ request.Context.Session is null ||
var generatedOpenApiSpecs = new Dictionary<string, string>();
foreach (var openApiDoc in openApiDocs)
{
// Allow derived plugins to post-process the OpenApiDocument (above the path level)
await ProcessOpenApiDocumentAsync(openApiDoc);

var server = openApiDoc.Servers.First();
var fileName = GetFileNameFromServerUrl(server.Url, Configuration.SpecFormat);

Expand Down Expand Up @@ -176,30 +200,70 @@ request.Context.Session is null ||
Logger.LogTrace("Left {Name}", nameof(AfterRecordingStopAsync));
}

private async Task<string> GetOperationIdAsync(string method, string serverUrl, string parametrizedPath)
/// <summary>
/// Allows derived plugins to post-process the OpenApiPathItem before it is added/merged into the document.
/// </summary>
/// <param name="pathItem">The OpenApiPathItem to process.</param>
/// <param name="requestUri">The request URI.</param>
/// <param name="parametrizedPath">The parametrized path string.</param>
/// <returns>The processed OpenApiPathItem.</returns>
protected virtual async Task ProcessPathItemAsync(OpenApiPathItem pathItem, Uri requestUri, string parametrizedPath)
{
ArgumentNullException.ThrowIfNull(pathItem);
ArgumentNullException.ThrowIfNull(requestUri);

var operationInfo = pathItem.Operations.First();
operationInfo.Value.OperationId = await GetOperationIdAsync(
operationInfo.Key.ToString(),
requestUri.GetLeftPart(UriPartial.Authority),
parametrizedPath
);
operationInfo.Value.Description = await GetOperationDescriptionAsync(
operationInfo.Key.ToString(),
requestUri.GetLeftPart(UriPartial.Authority),
parametrizedPath
);
}

/// <summary>
/// Allows derived plugins to post-process the OpenApiDocument before it is serialized and written to disk.
/// </summary>
/// <param name="openApiDoc">The OpenApiDocument to process.</param>
protected virtual Task ProcessOpenApiDocumentAsync(OpenApiDocument openApiDoc)
{
// By default, do nothing. Derived plugins can override to add/modify document-level data.
return Task.CompletedTask;
}

protected virtual async Task<string> GetOperationIdAsync(string method, string serverUrl, string parametrizedPath, string promptyFile = "api_operation_id")
{
ArgumentException.ThrowIfNullOrEmpty(method);
ArgumentException.ThrowIfNullOrEmpty(parametrizedPath);

ILanguageModelCompletionResponse? id = null;
if (await languageModelClient.IsEnabledAsync())
{
id = await languageModelClient.GenerateChatCompletionAsync("api_operation_id", new()
id = await languageModelClient.GenerateChatCompletionAsync(promptyFile, new()
{
{ "request", $"{method.ToUpperInvariant()} {serverUrl}{parametrizedPath}" }
});
}
return id?.Response ?? $"{method}{parametrizedPath.Replace('/', '.')}";
return id?.Response?.Trim() ?? $"{method}{parametrizedPath.Replace('/', '.')}";
}

private async Task<string> GetOperationDescriptionAsync(string method, string serverUrl, string parametrizedPath)
protected virtual async Task<string> GetOperationDescriptionAsync(string method, string serverUrl, string parametrizedPath, string promptyFile = "api_operation_description")
{
ArgumentException.ThrowIfNullOrEmpty(method);

ILanguageModelCompletionResponse? description = null;
if (await languageModelClient.IsEnabledAsync())
{
description = await languageModelClient.GenerateChatCompletionAsync("api_operation_description", new()
description = await languageModelClient.GenerateChatCompletionAsync(promptyFile, new()
{
{ "request", $"{method.ToUpperInvariant()} {serverUrl}{parametrizedPath}" }
});
}
return description?.Response ?? $"{method} {parametrizedPath}";
return description?.Response?.Trim() ?? $"{method} {parametrizedPath}";
}

/**
Expand Down
Loading