Skip to content
Merged
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
77 changes: 55 additions & 22 deletions docs/configure/content-set/api-explorer.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This feature is still under development and the functionality described on this
Add the `api` key to your `docset.yml` file to enable the API Explorer. The key maps product names to OpenAPI JSON specification files.
Paths are relative to the folder that contains `docset.yml`.

### Basic Configuration
### Basic configuration

```yaml
api:
Expand Down Expand Up @@ -138,9 +138,60 @@ The API Explorer generates the following types of pages from your OpenAPI spec:

## OpenAPI extensions

The API Explorer supports the following OpenAPI specification extensions to enhance navigation and display:
The API Explorer supports some OpenAPI specification extensions to enhance navigation and display:

### `x-displayName` for tags
- [x-codeSamples](#x-codesamples)
- [x-displayName](#x-displayname)
- [x-req-auth](#x-req-auth)
- [x-tagGroups](#x-taggroups)

For background on OpenAPI vendor extensions, refer to [OpenAPI Specification](https://spec.openapis.org/oas/latest.html#specification-extensions).

### Multi-language code examples [x-codesamples]

When an OpenAPI operation includes the `x-codeSamples` extension, the API Explorer renders the code samples with a language selector tab. This lets users switch between available languages such as Console, cURL, Python, JavaScript, Ruby, PHP, and Java.

The `x-codeSamples` extension is a JSON array of objects, each with a `lang` and `source` field:

```json
"x-codeSamples": [
{ "lang": "Console", "source": "GET /_search" },
{ "lang": "curl", "source": "curl -X GET ..." },
{ "lang": "Python", "source": "resp = client.search()" }
]
```

The code samples appear in a standalone "Code Examples" section on every operation page that has the extension, regardless of HTTP method. This means GET, DELETE, and other operations without a request body also display language tabs when `x-codeSamples` are present. When multiple languages are available, they appear as tabs. The selected language persists across operations and page navigations. When only one language is available, the example renders without a tab selector.

Console is treated as the default language and appears first in the tab order when present.

### Prerequisites [x-req-auth]

Add the operation-level `x-req-auth` extension to list authentication or privilege requirements that users must satisfy before calling the API.
The API Explorer renders these lines in a **Prerequisites** section on the operation page.

`x-req-auth` is a JSON array of strings.
Each non-empty string becomes one item in the prerequisites list (leading and trailing whitespace is trimmed).

```json
{
"get": {
"operationId": "get-snapshot",
"responses": { "200": { "description": "ok" } },
"x-req-auth": [
"Cluster privilege: `cluster:admin/snapshot`"
]
}
}
```



When prerequisites are present, **Prerequisites** also appears in the on-page table of contents (after **Paths**).
When the extension is missing, empty, or not a JSON array, the section is omitted.
Malformed values are skipped and the build may log a warning.

### Tag labels [x-displayname]

Use the `x-displayName` extension (from [Redocly](https://redocly.com/docs-legacy/api-reference-docs/specification-extensions/x-display-name)) on tag objects to provide user-friendly display names in navigation and landing pages while maintaining stable URLs based on the canonical tag name.

Expand All @@ -167,7 +218,7 @@ Use the `x-displayName` extension (from [Redocly](https://redocly.com/docs-legac
- When `x-displayName` is absent, the canonical tag `name` is used as a fallback
- Navigation URLs and internal references always use the canonical tag `name` for stability

### `x-tagGroups` for sidebar grouping
### Tag groups [x-taggroups]

Use the document-level `x-tagGroups` extension (from [Redocly](https://redocly.com/docs-legacy/api-reference-docs/specification-extensions/x-tag-groups)) to define how tags are grouped in the API Explorer sidebar. Each group has a display `name` and a list of tag `name` values that belong to it. Group order in the array is the order of top-level sections in the navigation.

Expand All @@ -194,21 +245,3 @@ Use the document-level `x-tagGroups` extension (from [Redocly](https://redocly.c
- When `x-tagGroups` is present and valid, the API Explorer uses it as an additional level of grouping in the sidebar.
- When `x-tagGroups` is absent, tags are listed directly under the API root in a single flat layer.
- Any operation tag that is not listed under any group is still included: it appears under a fallback section named `unknown`, and the build logs a warning so you can fix the spec.

### Multi-language code examples

When an OpenAPI operation includes the `x-codeSamples` extension, the API Explorer renders the code samples with a language selector tab. This lets users switch between available languages such as Console, cURL, Python, JavaScript, Ruby, PHP, and Java.

The `x-codeSamples` extension is a JSON array of objects, each with a `lang` and `source` field:

```json
"x-codeSamples": [
{ "lang": "Console", "source": "GET /_search" },
{ "lang": "curl", "source": "curl -X GET ..." },
{ "lang": "Python", "source": "resp = client.search()" }
]
```

The code samples appear in a standalone "Code Examples" section on every operation page that has the extension, regardless of HTTP method. This means GET, DELETE, and other operations without a request body also display language tabs when `x-codeSamples` are present. When multiple languages are available, they appear as tabs. The selected language persists across operations and page navigations. When only one language is available, the example renders without a tab selector.

Console is treated as the default language and appears first in the tab order when present.
3 changes: 3 additions & 0 deletions src/Elastic.ApiExplorer/ApiRenderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Elastic.Documentation.Navigation;
using Elastic.Documentation.Site.FileProviders;
using Elastic.Documentation.Site.Navigation;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi;

namespace Elastic.ApiExplorer;
Expand All @@ -23,4 +24,6 @@ StaticFileContentHashProvider StaticFileContentHashProvider
public required INavigationItem CurrentNavigation { get; init; }
public required IMarkdownStringRenderer MarkdownRenderer { get; init; }

/// <summary>Logger for API Explorer rendering (e.g. OpenAPI extension parsing); optional when the host does not provide one.</summary>
public ILogger? ApiExplorerLog { get; init; }
}
3 changes: 3 additions & 0 deletions src/Elastic.ApiExplorer/ApiViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ public abstract partial class ApiViewModel(ApiRenderContext context)
public BuildContext BuildContext { get; } = context?.BuildContext ?? throw new ArgumentNullException(nameof(context), "BuildContext cannot be null");
public OpenApiDocument Document { get; } = context?.Model ?? throw new ArgumentNullException(nameof(context), "OpenApiDocument cannot be null");

/// <summary>Current API render context (OpenAPI model, nav, optional logging).</summary>
protected ApiRenderContext RenderContext { get; } = context ?? throw new ArgumentNullException(nameof(context));


public HtmlString RenderMarkdown(string? markdown)
{
Expand Down
3 changes: 2 additions & 1 deletion src/Elastic.ApiExplorer/OpenApiGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,8 @@ private async Task GenerateApiProduct(string prefix, OpenApiDocument openApiDocu
{
NavigationHtml = string.Empty,
CurrentNavigation = navigation,
MarkdownRenderer = markdownStringRenderer
MarkdownRenderer = markdownStringRenderer,
ApiExplorerLog = _logger
};

await RenderNavigationItems(prefix, renderContext, navigationRenderer, navigation, navigation, ctx);
Expand Down
66 changes: 66 additions & 0 deletions src/Elastic.ApiExplorer/Operations/OpenApiXReqAuthParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System.Text.Json;
using System.Text.Json.Nodes;
using Elastic.Documentation.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi;

namespace Elastic.ApiExplorer.Operations;

public static class OpenApiXReqAuthParser
{
public const string ExtensionKey = "x-req-auth";

public static IReadOnlyList<string>? TryGetPrerequisiteLines(
OpenApiOperation operation,
ILogger? log,
string? route,
string? operationId
)
{
if (operation.Extensions is null)
return null;

if (!operation.Extensions.TryGetValue(ExtensionKey, out var ext) || ext is not JsonNodeExtension jne)
return null;

try
{
if (jne.Node is not JsonArray array)
{
log?.LogWarning("Failed to parse {Extension} extension for operation {OperationId} on path {Path}: expected a JSON array", ExtensionKey, operationId, route);
return null;
}

var list = new List<string>();
foreach (var node in array)
{
if (node is null)
continue;
var line = LineFromNode(node);
if (!string.IsNullOrWhiteSpace(line))
list.Add(line.Trim());
}

if (list.Count == 0)
return null;

return list;
}
catch (Exception ex)
{
log?.LogWarning(ex, "Failed to parse {Extension} extension for operation {OperationId} on path {Path}", ExtensionKey, operationId, route);
return null;
}
}

private static string LineFromNode(JsonNode node) =>
node is JsonValue value
? value.GetValueKind() == JsonValueKind.String
? value.GetValue<string>() ?? ""
: value.ToString() ?? ""
: node.ToString() ?? "";
}
20 changes: 20 additions & 0 deletions src/Elastic.ApiExplorer/Operations/OperationView.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,26 @@
@{
var pathParameters = operation.Parameters?.Where(p => p.In == ParameterLocation.Path).ToArray() ?? [];
}
@{
var prerequisiteLines = Model.RequiredAuthItems;
}
@if (prerequisiteLines is { Count: > 0 })
{
<h3 class="section-header" id="prerequisites" data-section="prerequisites">
<span>Prerequisites</span>
<span class="section-nav">
<span class="section-path">@Model.Operation.Route</span>
<button class="section-nav-btn" data-dir="up" title="Previous section">&#x25B2;</button>
<button class="section-nav-btn" data-dir="down" title="Next section">&#x25BC;</button>
</span>
</h3>
<ul class="prerequisite-list">
@foreach (var line in prerequisiteLines)
{
<li>@Model.RenderMarkdown(line)</li>
}
</ul>
}
@if (pathParameters.Length > 0)
{
<h4>Path Parameters</h4>
Expand Down
11 changes: 11 additions & 0 deletions src/Elastic.ApiExplorer/Operations/OperationViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,24 @@ public class OperationViewModel(ApiRenderContext context) : ApiViewModel(context
/// </summary>
public IReadOnlyList<CodeSample> CodeSamples { get; private set; } = [];

public IReadOnlyList<string>? RequiredAuthItems =>
OpenApiXReqAuthParser.TryGetPrerequisiteLines(
Operation.Operation,
RenderContext.ApiExplorerLog,
Operation.Route,
Operation.Operation.OperationId
);

protected override IReadOnlyList<ApiTocItem> GetTocItems()
{
CodeSamples = ParseCodeSamples(Operation.Operation);

var operation = Operation.Operation;
var tocItems = new List<ApiTocItem> { new("Paths", "paths") };

if (RequiredAuthItems is { Count: > 0 })
tocItems.Add(new ApiTocItem("Prerequisites", "prerequisites"));

if (!string.IsNullOrWhiteSpace(operation.Description))
tocItems.Add(new ApiTocItem("Description", "description"));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@
<PackageReference Include="Nullean.ScopedFileSystem" />
</ItemGroup>

<ItemGroup>
<Content Include="TestData\**\*.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"openapi": "3.0.0",
"info": {
"title": "Elasticsearch sample (x-req-auth cat indices)",
"version": "1.0.0"
},
"paths": {
"/_cat/indices": {
"get": {
"tags": [
"cat"
],
"summary": "Get index information",
"operationId": "cat-indices",
"responses": {
"200": {
"description": "OK"
}
},
"x-req-auth": [
"Index privileges: `monitor`\n",
"Cluster privileges: `monitor`\n"
]
}
},
"/_cat/indices/{index}": {
"get": {
"tags": [
"cat"
],
"summary": "Get index information",
"operationId": "cat-indices-1",
"parameters": [
{
"name": "index",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-req-auth": [
"Index privileges: `monitor`\n",
"Cluster privileges: `monitor`\n"
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"openapi": "3.0.0",
"info": {
"title": "Kibana sample (no x-req-auth)",
"version": "1.0.0"
},
"paths": {
"/api/status": {
"get": {
"operationId": "get-status",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/foo": {
"get": {
"operationId": "foo-get",
"responses": {
"200": {
"description": "OK"
}
}
},
"post": {
"operationId": "foo-post",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/bar": {
"delete": {
"operationId": "bar-delete",
"responses": {
"200": {
"description": "OK"
}
}
}
}
}
}
Loading
Loading