Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
138 changes: 138 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,144 @@ The [copy proposal](https://github.com/OAI/Overlay-Specification/pull/150) to th
}
```

### Action Parameters

The [action parameters proposal](https://github.com/OAI/Overlay-Specification/pull/238) adds support for parameterized overlay actions. This allows you to define parameters that can be used for string interpolation and to generate multiple actions through matrix expansion.

#### String Interpolation

Parameters can be referenced in action properties using the `${parameterName}` syntax. The parameter name matches an environment variable name, with optional default values as fallback:

```json
{
"target": "$.info.title",
"description": "Update title with environment",
"update": "API for ${environment}",
"x-parameters": [
{
"name": "environment",
"defaultValues": ["development", "staging", "production"]
}
]
}
```

#### Matrix Expansion

When an action has parameters with multiple values, the action is expanded into multiple actions (one for each combination of parameter values):

```json
{
"target": "$.paths./api/${version}/users.get.summary",
"description": "Update summary for each version and environment",
"update": "Get users - ${environment} (${version})",
"x-parameters": [
{
"name": "version",
"defaultValues": ["v1", "v2"]
},
{
"name": "environment",
"defaultValues": ["dev", "prod"]
}
]
}
```

This single action expands to 4 actions (v1+dev, v1+prod, v2+dev, v2+prod).

#### Environment Variables

Parameters always try to read from environment variables first. The `name` property specifies the environment variable name:

```json
{
"target": "$.servers[0].url",
"description": "Set server URL from environment",
"update": "https://${API_HOST}/api",
"x-parameters": [
{
"name": "API_HOST"
}
]
}
```

Environment variables can contain JSON arrays (validated same as defaultValues - must be array of strings or array of objects with string key/value pairs):

```json
// Set environment variable:
// ENVIRONMENTS='["dev", "staging", "prod"]'
{
"target": "$.info.title",
"description": "Update title for each environment",
"update": "API for ${ENVIRONMENTS}",
"x-parameters": [
{
"name": "ENVIRONMENTS"
}
]
}
```

You can provide default values that are used when the environment variable is not set. The `defaultValues` can be an array of strings or an array of objects (where each object contains only string key/value pairs):

```json
{
"target": "$.info.title",
"description": "Update title with environment or default",
"update": "API for ${environment}",
"x-parameters": [
{
"name": "environment",
"defaultValues": ["production"]
}
]
}
```

#### Dotted Notation for Object Properties

When using objects in parameters, you can access individual properties using dotted notation:

```json
{
"target": "$.info.title",
"description": "Update title with server info",
"update": "${server.name} at ${server.url}",
"x-parameters": [
{
"name": "server",
"defaultValues": [
{"url": "https://api1.example.com", "name": "Server 1"},
{"url": "https://api2.example.com", "name": "Server 2"}
]
}
]
}
```

This will expand to 2 actions:
- "Server 1 at https://api1.example.com"
- "Server 2 at https://api2.example.com"

Dotted notation also works with environment variables:

```json
// Set environment variable:
// SERVER='[{"url": "https://prod-api.com", "region": "us-east"}]'
{
"target": "$.info.title",
"description": "Server in region",
"update": "API in ${SERVER.region} at ${SERVER.url}",
"x-parameters": [
{
"name": "SERVER"
}
]
}
```

## Release notes

The OpenAPI Overlay Libraries releases notes are available from the [CHANGELOG](CHANGELOG.md)
Expand Down
21 changes: 21 additions & 0 deletions src/lib/Models/OverlayAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ public class OverlayAction : IOverlaySerializable, IOverlayExtensible
[Experimental("BOO001", UrlFormat = "https://github.com/OAI/Overlay-Specification/pull/150")]
public string? Copy { get; set; }

/// <summary>
/// Parameters that can be used for string interpolation within this action.
/// This field is experimental and not part of the OpenAPI Overlay specification v1.0.0.
/// This field is an implementation of <see href="https://github.com/OAI/Overlay-Specification/pull/238">the action parameters proposal</see>.
/// </summary>
[Experimental("BOO002", UrlFormat = "https://github.com/OAI/Overlay-Specification/pull/238")]
public List<OverlayParameter>? Parameters { get; set; }

/// <inheritdoc/>
public IDictionary<string, IOverlayExtension>? Extensions { get; set; }

Expand All @@ -71,6 +79,19 @@ public void SerializeAsV1(IOpenApiWriter writer)
}
#pragma warning restore BOO001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

#pragma warning disable BOO002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
if (Parameters != null && Parameters.Count > 0)
{
writer.WritePropertyName("x-parameters");
writer.WriteStartArray();
foreach (var parameter in Parameters)
{
parameter.SerializeAsV1(writer);
}
writer.WriteEndArray();
}
#pragma warning restore BOO002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

writer.WriteOverlayExtensions(Extensions, OverlaySpecVersion.Overlay1_0);
writer.WriteEndObject();
}
Expand Down
11 changes: 9 additions & 2 deletions src/lib/Models/OverlayDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,21 @@ internal bool ApplyToDocument(JsonNode jsonNode, OverlayDiagnostic overlayDiagno
}
var i = 0;
var result = true;
#pragma warning disable BOO002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
foreach (var action in Actions)
{
if (!action.ApplyToDocument(jsonNode, overlayDiagnostic, i))
// Expand action if it has parameters
var expandedActions = ParameterProcessor.ExpandActionWithParameters(action);
foreach (var expandedAction in expandedActions)
{
result = false; // If any action fails, the entire application fails
if (!expandedAction.ApplyToDocument(jsonNode, overlayDiagnostic, i))
{
result = false; // If any action fails, the entire application fails
}
}
i++;
}
#pragma warning restore BOO002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
return result;
}
/// <summary>
Expand Down
118 changes: 118 additions & 0 deletions src/lib/Models/OverlayParameter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Nodes;

using BinkyLabs.OpenApi.Overlays.Writers;

using Microsoft.OpenApi;

namespace BinkyLabs.OpenApi.Overlays;

/// <summary>
/// Represents a parameter for an overlay action.
/// This class is experimental and not part of the OpenAPI Overlay specification v1.0.0.
/// This class is an implementation of <see href="https://github.com/OAI/Overlay-Specification/pull/238">the action parameters proposal</see>.
/// </summary>
[Experimental("BOO002", UrlFormat = "https://github.com/OAI/Overlay-Specification/pull/238")]
public class OverlayParameter : IOverlaySerializable
{
/// <summary>
/// REQUIRED. The name of the parameter.
/// </summary>
public string? Name { get; set; }

/// <summary>
/// Default values for the parameter when the environment variable is not set.
/// Must be an array of strings or an array of objects where each object only contains key/value pairs of strings.
/// </summary>
public JsonNode? DefaultValues { get; set; }

/// <summary>
/// Validates that defaultValues is either an array of strings or an array of objects with string key/value pairs.
/// </summary>
internal static bool AreDefaultValuesValid(JsonNode? defaultValues)
{
if (defaultValues == null)
{
return true;
}

if (defaultValues is not JsonArray array)
{
return false;
}

if (array.Count == 0)
{
return true;
}

// Check if all elements are strings
var allStrings = true;
var allObjects = true;

foreach (var item in array)
{
if (item == null)
{
return false;
}

if (item is JsonValue jsonValue)
{
allObjects = false;
if (!jsonValue.TryGetValue<string>(out _))
{
allStrings = false;
}
}
else if (item is JsonObject jsonObject)
{
allStrings = false;
// Validate that all properties have string values
foreach (var prop in jsonObject)
{
if (prop.Value == null || prop.Value is not JsonValue propValue || !propValue.TryGetValue<string>(out _))
{
allObjects = false;
break;
}
}
}
else
{
return false;
}

if (!allStrings && !allObjects)
{
return false;
}
}

return allStrings || allObjects;
}

/// <summary>
/// Serializes the parameter object as an OpenAPI Overlay v1.0.0 JSON object.
/// </summary>
/// <param name="writer">The OpenAPI writer to use for serialization.</param>
public void SerializeAsV1(IOpenApiWriter writer)
{
writer.WriteStartObject();
writer.WriteRequiredProperty("name", Name);

if (DefaultValues != null)
{
if (!AreDefaultValuesValid(DefaultValues))
{
throw new InvalidOperationException(
"DefaultValues must be an array of strings or an array of objects where each object only contains key/value pairs of strings.");
}

writer.WritePropertyName("defaultValues");
writer.WriteAny(DefaultValues);
}

writer.WriteEndObject();
}
}
Loading
Loading