Skip to content

Commit 06e45f4

Browse files
authored
[API explorer] Add support for tag x-displayName (#3140)
1 parent f2fd8ff commit 06e45f4

4 files changed

Lines changed: 682 additions & 3 deletions

File tree

docs/configure/content-set/api-explorer.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,34 @@ The API Explorer generates the following types of pages from your OpenAPI spec:
112112
- **Landing page**: An overview of the API grouped by tag
113113
- **Operation pages**: One page per API operation, with the HTTP method, path, parameters, request body, response schemas, and examples
114114
- **Schema type pages**: Dedicated pages for complex shared types such as `QueryContainer` and `AggregationContainer`
115+
116+
## OpenAPI extensions
117+
118+
The API Explorer supports the following OpenAPI specification extensions to enhance navigation and display:
119+
120+
### `x-displayName` for tags
121+
122+
Use the `x-displayName` extension on tag objects to provide user-friendly display names in navigation and landing pages while maintaining stable URLs based on the canonical tag name.
123+
124+
```json
125+
{
126+
"tags": [
127+
{
128+
"name": "tasks",
129+
"description": "The task management APIs enable you to get information about tasks currently running.",
130+
"x-displayName": "Task management"
131+
},
132+
{
133+
"name": "ml_anomaly",
134+
"description": "Machine learning anomaly detection APIs.",
135+
"x-displayName": "Machine Learning Anomaly Detection"
136+
}
137+
]
138+
}
139+
```
140+
141+
**Behavior:**
142+
- When `x-displayName` is present, it's used for navigation titles and section headings in the API Explorer
143+
- When `x-displayName` is absent, the canonical tag `name` is used as a fallback
144+
- Navigation URLs and internal references always use the canonical tag `name` for stability
145+
- This extension follows the [Redocly specification extension pattern](https://redocly.com/docs-legacy/api-reference-docs/specification-extensions/x-display-name)

src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public class TagNavigationItem(ApiTag tag, IRootNavigationItem<IApiGroupingModel
117117
: ApiGroupingNavigationItem<ApiTag, IEndpointOrOperationNavigationItem>(tag, rootNavigation, parent)
118118
{
119119
/// <inheritdoc />
120-
public override string NavigationTitle { get; } = tag.Name;
120+
public override string NavigationTitle { get; } = tag.DisplayName;
121121

122122
/// <inheritdoc />
123123
public override string Id { get; } = ShortId.Create(tag.Name);

src/Elastic.ApiExplorer/OpenApiGenerator.cs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5+
using System;
56
using System.IO.Abstractions;
67
using System.Text.RegularExpressions;
78
using Elastic.ApiExplorer.Landing;
@@ -29,7 +30,7 @@ public record ApiClassification(string Name, string Description, IReadOnlyCollec
2930
public Task RenderAsync(FileSystemStream stream, ApiRenderContext context, CancellationToken ctx = default) => Task.CompletedTask;
3031
}
3132

32-
public record ApiTag(string Name, string Description, IReadOnlyCollection<ApiEndpoint> Endpoints) : IApiGroupingModel
33+
public record ApiTag(string Name, string DisplayName, string Description, IReadOnlyCollection<ApiEndpoint> Endpoints) : IApiGroupingModel
3334
{
3435
/// <inheritdoc />
3536
public Task RenderAsync(FileSystemStream stream, ApiRenderContext context, CancellationToken ctx = default) => Task.CompletedTask;
@@ -53,6 +54,9 @@ public LandingNavigationItem CreateNavigation(string apiUrlSuffix, OpenApiDocume
5354
var url = $"{context.UrlPathPrefix}/api/" + apiUrlSuffix;
5455
var rootNavigation = new LandingNavigationItem(url);
5556

57+
// Parse x-displayName from OpenAPI tags for user-friendly display names
58+
var tagDisplayNames = ParseTagDisplayNames(openApiDocument);
59+
5660
var ops = openApiDocument.Paths
5761
.SelectMany(p => (p.Value.Operations ?? []).Select(op => (Path: p, Operation: op)))
5862
.Select(pair =>
@@ -106,9 +110,15 @@ public LandingNavigationItem CreateNavigation(string apiUrlSuffix, OpenApiDocume
106110
var apiEndpoint = new ApiEndpoint(operations, apiGroup.Key);
107111
apis.Add(apiEndpoint);
108112
}
109-
var tag = new ApiTag(tagGroup.Key ?? "unknown", "", apis);
113+
var tagName = tagGroup.Key ?? "unknown";
114+
var displayName = tagDisplayNames.TryGetValue(tagName, out var foundDisplayName) ? foundDisplayName : tagName;
115+
var tag = new ApiTag(tagName, displayName, "", apis);
110116
tags.Add(tag);
111117
}
118+
119+
// Sort tags alphabetically by display name, fallback to canonical name
120+
tags = tags.OrderBy(t => t.DisplayName ?? t.Name, StringComparer.OrdinalIgnoreCase).ToList();
121+
112122
var classification = new ApiClassification(classGroup.Key, "", tags);
113123
classifications.Add(classification);
114124
}
@@ -461,4 +471,40 @@ private static string ClassifyElasticsearchTag(string tag)
461471
}
462472
return "unknown";
463473
}
474+
475+
/// <summary>
476+
/// Parses x-displayName extensions from OpenAPI tag objects to build a mapping of tag names to display names.
477+
/// Falls back to the canonical tag name when no x-displayName is present.
478+
/// </summary>
479+
private static Dictionary<string, string> ParseTagDisplayNames(OpenApiDocument openApiDocument)
480+
{
481+
var displayNames = new Dictionary<string, string>();
482+
483+
if (openApiDocument.Tags is null)
484+
return displayNames;
485+
486+
foreach (var tag in openApiDocument.Tags)
487+
{
488+
var tagName = tag.Name;
489+
if (string.IsNullOrEmpty(tagName))
490+
continue;
491+
492+
var displayName = tagName; // Default fallback
493+
494+
// Look for x-displayName extension
495+
if (tag.Extensions?.TryGetValue("x-displayName", out var extension) == true &&
496+
extension is JsonNodeExtension jsonExtension)
497+
{
498+
var displayNameValue = jsonExtension.Node.GetValue<string>();
499+
if (!string.IsNullOrWhiteSpace(displayNameValue))
500+
{
501+
displayName = displayNameValue;
502+
}
503+
}
504+
505+
displayNames[tagName] = displayName;
506+
}
507+
508+
return displayNames;
509+
}
464510
}

0 commit comments

Comments
 (0)