mirror of
https://github.com/bitwarden/server
synced 2025-12-24 20:23:21 +00:00
Update Swashbuckle and improve generated OpenAPI files (#6066)
* Improve generated OpenAPI files * Nullable * Fmt * Correct powershell command * Fix name * Add some tests * Fmt * Switch to using json naming policy
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="7.3.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="9.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
40
src/SharedWeb/Swagger/EncryptedStringSchemaFilter.cs
Normal file
40
src/SharedWeb/Swagger/EncryptedStringSchemaFilter.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Bit.SharedWeb.Swagger;
|
||||
|
||||
/// <summary>
|
||||
/// Set the format of any strings that are decorated with the <see cref="EncryptedStringAttribute"/> to "x-enc-string".
|
||||
/// This will allow the generated bindings to use a more appropriate type for encrypted strings.
|
||||
/// </summary>
|
||||
public class EncryptedStringSchemaFilter : ISchemaFilter
|
||||
{
|
||||
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
|
||||
{
|
||||
if (context.Type == null || schema.Properties == null)
|
||||
return;
|
||||
|
||||
foreach (var prop in context.Type.GetProperties())
|
||||
{
|
||||
// Only apply to string properties
|
||||
if (prop.PropertyType != typeof(string))
|
||||
continue;
|
||||
|
||||
// Check if the property has the EncryptedString attribute
|
||||
if (prop.GetCustomAttributes(typeof(EncryptedStringAttribute), true).FirstOrDefault() != null)
|
||||
{
|
||||
// Convert prop.Name to camelCase for JSON schema property lookup
|
||||
var jsonPropName = JsonNamingPolicy.CamelCase.ConvertName(prop.Name);
|
||||
|
||||
if (schema.Properties.TryGetValue(jsonPropName, out var value))
|
||||
{
|
||||
value.Format = "x-enc-string";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/SharedWeb/Swagger/GitCommitDocumentFilter.cs
Normal file
50
src/SharedWeb/Swagger/GitCommitDocumentFilter.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Bit.SharedWeb.Swagger;
|
||||
|
||||
/// <summary>
|
||||
/// Add the Git commit that was used to generate the Swagger document, to help with debugging and reproducibility.
|
||||
/// </summary>
|
||||
public class GitCommitDocumentFilter : IDocumentFilter
|
||||
{
|
||||
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(GitCommit))
|
||||
{
|
||||
swaggerDoc.Extensions.Add("x-git-commit", new Microsoft.OpenApi.Any.OpenApiString(GitCommit));
|
||||
}
|
||||
}
|
||||
|
||||
public static string? GitCommit => _gitCommit.Value;
|
||||
|
||||
private static readonly Lazy<string?> _gitCommit = new(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "git",
|
||||
Arguments = "rev-parse HEAD",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
process.Start();
|
||||
var result = process.StandardOutput.ReadLine()?.Trim();
|
||||
process.WaitForExit();
|
||||
return result ?? string.Empty;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
87
src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs
Normal file
87
src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Reflection;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Reflection.Metadata.Ecma335;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Bit.SharedWeb.Swagger;
|
||||
|
||||
/// <summary>
|
||||
/// Adds source file and line number information to the Swagger operation description.
|
||||
/// This can be useful for locating the source code of the operation in the repository,
|
||||
/// as the generated names are based on the HTTP path, and are hard to search for.
|
||||
/// </summary>
|
||||
public class SourceFileLineOperationFilter : IOperationFilter
|
||||
{
|
||||
private static readonly string _gitCommit = GitCommitDocumentFilter.GitCommit ?? "main";
|
||||
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
|
||||
var (fileName, lineNumber) = GetSourceFileLine(context.MethodInfo);
|
||||
if (fileName != null && lineNumber > 0)
|
||||
{
|
||||
// Add the information with a link to the source file at the end of the operation description
|
||||
operation.Description +=
|
||||
$"\nThis operation is defined on: [`https://github.com/bitwarden/server/blob/{_gitCommit}/{fileName}#L{lineNumber}`]";
|
||||
|
||||
// Also add the information as extensions, so other tools can use it in the future
|
||||
operation.Extensions.Add("x-source-file", new OpenApiString(fileName));
|
||||
operation.Extensions.Add("x-source-line", new OpenApiInteger(lineNumber));
|
||||
}
|
||||
}
|
||||
|
||||
private static (string? fileName, int lineNumber) GetSourceFileLine(MethodInfo methodInfo)
|
||||
{
|
||||
// Get the location of the PDB file associated with the module of the method
|
||||
var pdbPath = Path.ChangeExtension(methodInfo.Module.FullyQualifiedName, ".pdb");
|
||||
if (!File.Exists(pdbPath)) return (null, 0);
|
||||
|
||||
// Open the PDB file and read the metadata
|
||||
using var pdbStream = File.OpenRead(pdbPath);
|
||||
using var metadataReaderProvider = MetadataReaderProvider.FromPortablePdbStream(pdbStream);
|
||||
var metadataReader = metadataReaderProvider.GetMetadataReader();
|
||||
|
||||
// If the method is async, the compiler will generate a state machine,
|
||||
// so we can't look for the original method, but we instead need to look
|
||||
// for the MoveNext method of the state machine.
|
||||
var attr = methodInfo.GetCustomAttribute<AsyncStateMachineAttribute>();
|
||||
if (attr?.StateMachineType != null)
|
||||
{
|
||||
var moveNext = attr.StateMachineType.GetMethod("MoveNext", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
if (moveNext != null) methodInfo = moveNext;
|
||||
}
|
||||
|
||||
// Once we have the method, we can get its sequence points
|
||||
var handle = (MethodDefinitionHandle)MetadataTokens.Handle(methodInfo.MetadataToken);
|
||||
if (handle.IsNil) return (null, 0);
|
||||
var sequencePoints = metadataReader.GetMethodDebugInformation(handle).GetSequencePoints();
|
||||
|
||||
// Iterate through the sequence points and pick the first one that has a valid line number
|
||||
foreach (var sp in sequencePoints)
|
||||
{
|
||||
var docName = metadataReader.GetDocument(sp.Document).Name;
|
||||
if (sp.StartLine != 0 && sp.StartLine != SequencePoint.HiddenLine && !docName.IsNil)
|
||||
{
|
||||
var fileName = metadataReader.GetString(docName);
|
||||
var repoRoot = FindRepoRoot(AppContext.BaseDirectory);
|
||||
var relativeFileName = repoRoot != null ? Path.GetRelativePath(repoRoot, fileName) : fileName;
|
||||
return (relativeFileName, sp.StartLine);
|
||||
}
|
||||
}
|
||||
|
||||
return (null, 0);
|
||||
}
|
||||
|
||||
private static string? FindRepoRoot(string startPath)
|
||||
{
|
||||
var dir = new DirectoryInfo(startPath);
|
||||
while (dir != null && !Directory.Exists(Path.Combine(dir.FullName, ".git")))
|
||||
dir = dir.Parent;
|
||||
return dir?.FullName;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user