Skip to content

Commit

Permalink
Swagger transformations (#36)
Browse files Browse the repository at this point in the history
* Swagger transformation

* Swagger transformation example

* Update readme with swagger transformation

* Improved swagger transformation efficiency

* fix of sample apps

* Prevent exception for missing factories and return early

* Refactored ApplySwaggerTransformation
  • Loading branch information
jwillmer authored Apr 3, 2024
1 parent 2048628 commit fa8367b
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 15 deletions.
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,4 +331,65 @@ Update appsettings.json:
},
}
}
```

# Swagger Transformation

If you want to transform the Swagger to fit the modifications done by the transformations you have to implement the changes in the `ISwaggerTransformFactory`. Below is an example from a custom transformation called `RenameHeader`.

```csharp
public class HeaderTransformFactory : ITransformFactory, ISwaggerTransformFactory
{
public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary<string, string> transformValues)
{
// validation implementation of custom transformation
}

public bool Build(TransformBuilderContext context, IReadOnlyDictionary<string, string> transformValues)
{
// transform implementation of custom transformation
}

/// <summary>
/// Header title rename transformation for Swagger
/// </summary>
/// <param name="operation"></param>
/// <param name="transformValues"></param>
/// <returns></returns>
public bool Build(OpenApiOperation operation, IReadOnlyDictionary<string, string> transformValues)
{
if (transformValues.ContainsKey("RenameHeader"))
{
foreach (var parameter in operation.Parameters)
{
if (parameter.In.HasValue && parameter.In.Value.ToString().Equals("Header"))
{
if (transformValues.TryGetValue("RenameHeader", out var header)
&& transformValues.TryGetValue("Set", out var newHeader))
{
if (parameter.Name == newHeader)
{
parameter.Name = header;
}
}
}
}

return true;
}

return false;
}
}

```

Then register the transformation in `Program.cs`

```
builder.Services
.AddReverseProxy()
.LoadFromConfig(configuration)
.AddTransformFactory<HeaderTransformFactory>()
.AddSwagger(configuration)
```
4 changes: 2 additions & 2 deletions sample/App2/Controllers/WeatherForecastController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ public WeatherForecastController(ILogger<WeatherForecastController> logger)
}

[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
public IEnumerable<WeatherForecast> Get([FromHeader(Name = "x-range")] int range = 5)
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
return Enumerable.Range(1, range).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Expand Down
2 changes: 2 additions & 0 deletions sample/Yarp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Yarp.Extensions;
using Yarp.ReverseProxy.Swagger;
using Yarp.ReverseProxy.Swagger.Extensions;
using Yarp.Transformations;

var builder = WebApplication.CreateBuilder(args);

Expand All @@ -31,6 +32,7 @@
.AddReverseProxy()
.LoadFromConfig(configuration)
.LoadFromConfig(configurationForOnlyPublishedRoutes)
.AddTransformFactory<HeaderTransformFactory>()
.AddSwagger(configuration)
.AddSwagger(configurationForOnlyPublishedRoutes);

Expand Down
122 changes: 122 additions & 0 deletions sample/Yarp/Transformations/HeaderTransformFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using Microsoft.OpenApi.Models;
using System.Net;
using Yarp.ReverseProxy.Swagger;
using Yarp.ReverseProxy.Transforms;
using Yarp.ReverseProxy.Transforms.Builder;

namespace Yarp.Transformations;

public class HeaderTransformFactory : ITransformFactory, ISwaggerTransformFactory
{
/// <summary>
/// Property validates for header titel rename
/// </summary>
/// <param name="context"></param>
/// <param name="transformValues"></param>
/// <returns></returns>
public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary<string, string> transformValues)
{
if (transformValues.TryGetValue("RenameHeader", out var header))
{
if (string.IsNullOrEmpty(header))
{
context.Errors.Add(new ArgumentException("A non-empty RenameHeader value is required"));
}

if (transformValues.TryGetValue("Set", out var newHeader))
{
if (string.IsNullOrEmpty(newHeader))
{
context.Errors.Add(new ArgumentException("A non-empty Set value is required"));
}
}
else
{
context.Errors.Add(new ArgumentException("Set option is required"));
}

return true;
}

return false;
}

/// <summary>
/// Header title rename transformation
/// </summary>
/// <param name="context"></param>
/// <param name="transformValues"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public bool Build(TransformBuilderContext context, IReadOnlyDictionary<string, string> transformValues)
{
if (transformValues.TryGetValue("RenameHeader", out var header))
{
if (string.IsNullOrEmpty(header))
{
throw new ArgumentException("A non-empty RenameHeader value is required");
}

if (transformValues.TryGetValue("Set", out var newHeader))
{
if (string.IsNullOrEmpty(newHeader))
{
throw new ArgumentException("A non-empty Set value is required");
}
}
else
{
throw new ArgumentException("Set option is required");
}

context.AddRequestTransform(transformContext =>
{
if (transformContext.ProxyRequest.Headers.TryGetValues(header, out var headerValue))
{
// Remove the original header
transformContext.ProxyRequest.Headers.Remove(header);

// Add a new header with the same value(s) as the original header
transformContext.ProxyRequest.Headers.Add(newHeader, headerValue);
}

return default;
});

return true;
}

return false;
}

/// <summary>
/// Header title rename transformation for Swagger
/// </summary>
/// <param name="operation"></param>
/// <param name="transformValues"></param>
/// <returns></returns>
public bool Build(OpenApiOperation operation, IReadOnlyDictionary<string, string> transformValues)
{
if (transformValues.ContainsKey("RenameHeader"))
{
foreach (var parameter in operation.Parameters)
{
if (parameter.In.HasValue && parameter.In.Value.ToString().Equals("Header"))
{
if (transformValues.TryGetValue("RenameHeader", out var header)
&& transformValues.TryGetValue("Set", out var newHeader))
{
if (parameter.Name == newHeader)
{
parameter.Name = header;
}
}
}
}

return true;
}

return false;
}
}
10 changes: 7 additions & 3 deletions sample/Yarp/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@
"Path": "/proxy-app2/{**catch-all}"
},
"Transforms": [
{
"PathPattern": "{**catch-all}"
}
{
"PathPattern": "{**catch-all}"
},
{
"RenameHeader": "days",
"Set": "x-range"
}
]
}
},
Expand Down
10 changes: 10 additions & 0 deletions src/Yarp.ReverseProxy.Swagger/ISwaggerTransformFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Microsoft.OpenApi.Models;
using System.Collections.Generic;

namespace Yarp.ReverseProxy.Swagger
{
public interface ISwaggerTransformFactory
{
bool Build(OpenApiOperation operation, IReadOnlyDictionary<string, string> transformValues);
}
}
54 changes: 44 additions & 10 deletions src/Yarp.ReverseProxy.Swagger/ReverseProxyDocumentFilter.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
Expand All @@ -6,7 +7,9 @@
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
using Swashbuckle.AspNetCore.SwaggerGen;
using Yarp.ReverseProxy.Configuration;
using Yarp.ReverseProxy.Swagger.Extensions;
using Yarp.ReverseProxy.Transforms.Builder;

namespace Yarp.ReverseProxy.Swagger
{
Expand All @@ -15,16 +18,19 @@ public sealed class ReverseProxyDocumentFilter : IDocumentFilter
private readonly IHttpClientFactory _httpClientFactory;
private ReverseProxyDocumentFilterConfig _config;
private readonly IReadOnlyDictionary<string, OperationType> _operationTypeMapping;
private readonly List<ITransformFactory> _factories;

public ReverseProxyDocumentFilter(
IHttpClientFactory httpClientFactory,
IOptionsMonitor<ReverseProxyDocumentFilterConfig> configOptions)
IOptionsMonitor<ReverseProxyDocumentFilterConfig> configOptions,
IEnumerable<ITransformFactory> factories)
{
_factories = factories?.ToList();
_config = configOptions.CurrentValue;
_httpClientFactory = httpClientFactory;

configOptions.OnChange(config => { _config = config; });

_operationTypeMapping = new Dictionary<string, OperationType>
{
{"GET", OperationType.Get},
Expand All @@ -47,7 +53,7 @@ DocumentFilterContext context
{
return;
}

IReadOnlyDictionary<string, ReverseProxyDocumentFilterConfig.Cluster> clusters;

if (_config.Swagger.IsCommonDocument)
Expand All @@ -60,10 +66,10 @@ DocumentFilterContext context
.Where(x => x.Key == context.DocumentName)
.ToDictionary(x => x.Key, x => x.Value);
}

Apply(swaggerDoc, clusters);
}

private void Apply(
OpenApiDocument swaggerDoc,
IReadOnlyDictionary<string, ReverseProxyDocumentFilterConfig.Cluster> clusters
Expand All @@ -73,7 +79,7 @@ private void Apply(
{
return;
}

var info = swaggerDoc.Info;
var paths = new OpenApiPaths();
var components = new OpenApiComponents();
Expand All @@ -84,12 +90,12 @@ private void Apply(
{
var clusterKey = clusterKeyValuePair.Key;
var cluster = clusterKeyValuePair.Value;

if (true != cluster.Destinations?.Any())
{
continue;
}

foreach (var destination in cluster.Destinations)
{
if (true != destination.Value.Swaggers?.Any())
Expand Down Expand Up @@ -139,6 +145,7 @@ private void Apply(
continue;
}

var operationKeys = path.Value.Operations.Keys.ToList();
if (publishedRoutes != null)
{
var pathKey = $"{swagger.PrefixPath}{path.Key}";
Expand All @@ -152,7 +159,6 @@ private void Apply(
.Where(q => methods.Contains(q.Key))
.Select(q => q.Value)
.ToList();
var operationKeys = path.Value.Operations.Keys.ToList();

foreach (var operationKey in operationKeys)
{
Expand All @@ -163,6 +169,8 @@ private void Apply(
}
}

ApplySwaggerTransformation(operationKeys, path, clusterKey);

paths.TryAdd($"{swagger.PrefixPath}{key}", value);
}

Expand Down Expand Up @@ -205,5 +213,31 @@ private static IReadOnlyDictionary<string, IEnumerable<string>> GetPublishedPath

return validRoutes;
}

private void ApplySwaggerTransformation(List<OperationType> operationKeys, KeyValuePair<string, OpenApiPathItem> path, string clusterKey)
{
var factories = _factories?.Where(x => x is ISwaggerTransformFactory).ToList();

if (factories == null) return;

foreach (var operationKey in operationKeys)
{
path.Value.Operations.TryGetValue(operationKey, out var operation);

var transforms = _config.Routes
.Where(x => x.Value.ClusterId == clusterKey)
.Where(x => x.Value.Transforms != null)
.SelectMany(x => x.Value.Transforms)
.ToList();

foreach (var swaggerFactory in factories.Select(factory => factory as ISwaggerTransformFactory))
{
foreach (var transform in transforms)
{
swaggerFactory?.Build(operation, transform);
}
}
}
}
}
}

0 comments on commit fa8367b

Please sign in to comment.