Skip to content
Open
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
6 changes: 3 additions & 3 deletions docs/data/openapi/api-explorer.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ navigation_title: API Explorer

# API Explorer

The API Explorer renders OpenAPI specifications as interactive API documentation. When you configure it in your content set, `docs-builder` automatically generates a product landing page, `/authentication` and `/servers` pages, tag and operation pages, request and response schemas, shared type definitions, and inline examples.
The API Explorer renders OpenAPI specifications as interactive API documentation. When you configure it in your content set, `docs-builder` automatically generates a product landing page, an `/authentication` page when the spec declares security schemes, a `/servers` page when the spec declares servers, tag and operation pages, request and response schemas, shared type definitions, and inline examples.

The assembler also writes a combined **API catalog** at `/docs/api/`: a grid of product cards on its own layout (no API sidebar). Each card opens the HTML landing page and includes REST and category badges plus JSON and YAML downloads. Markdown, JSON, and YAML stay on the product landing page and in the catalog Markdown export. The card shows `info.description`, clamped to three lines.

Expand Down Expand Up @@ -106,8 +106,8 @@ The following slugs are reserved and cannot be used as child file names:
| `types` | API Explorer uses this path for schema type pages |
| `group` | Tag landing pages use `/group/` |
| `operation` | Operation pages use `/operation/` |
| `authentication` | Each API product has an `/authentication` page |
| `servers` | Each API product has a `/servers` page |
| `authentication` | Reserved for API Explorer and cannot be used as a child file slug |
| `servers` | Reserved for API Explorer and cannot be used as a child file slug |

Additionally, the slug must not match any operation moniker already generated by the spec. The
build fails with a descriptive error if either collision occurs, naming the conflicting file and
Expand Down
5 changes: 3 additions & 2 deletions src/Elastic.ApiExplorer/Navigation/ApiNavigationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,10 @@ public LandingNavigationItem CreateNavigation(
}
}

finalNavigationItems.AddRange(StructuralNavigationItem.Create(context.UrlPathPrefix, apiUrlSuffix, rootNavigation));
finalNavigationItems.AddRange(
StructuralNavigationItem.Create(context.UrlPathPrefix, apiUrlSuffix, rootNavigation, openApiDocument)
);

// Add existing navigation items (OpenAPI generated content)
if (topLevelNavigationItems.Count > 0)
finalNavigationItems.AddRange(topLevelNavigationItems);
else if (rootNavigation.NavigationItems.Count > 0)
Expand Down
24 changes: 17 additions & 7 deletions src/Elastic.ApiExplorer/Structural/ApiStructuralPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,20 @@ INodeNavigationItem<INavigationModel, INavigationItem> parent
public INodeNavigationItem<INavigationModel, INavigationItem>? Parent { get; set; }
public int NavigationIndex { get; set; }

public static IReadOnlyList<StructuralNavigationItem> Create(string? urlPathPrefix, string apiUrlSuffix, LandingNavigationItem root) =>
[
new(urlPathPrefix, apiUrlSuffix, new ApiStructuralPage(ApiStructuralKind.Authentication), root, root),
new(urlPathPrefix, apiUrlSuffix, new ApiStructuralPage(ApiStructuralKind.Servers), root, root)
];
public static IReadOnlyList<StructuralNavigationItem> Create(
string? urlPathPrefix,
string apiUrlSuffix,
LandingNavigationItem root,
OpenApiDocument document
)
{
var items = new List<StructuralNavigationItem>();
if (StructuralViewModel.HasSchemes(document))
items.Add(new(urlPathPrefix, apiUrlSuffix, new ApiStructuralPage(ApiStructuralKind.Authentication), root, root));
if (StructuralViewModel.ReadServers(document).Count > 0)
items.Add(new(urlPathPrefix, apiUrlSuffix, new ApiStructuralPage(ApiStructuralKind.Servers), root, root));
return items;
}
}

public class StructuralViewModel(ApiRenderContext context) : ApiViewModel(context)
Expand Down Expand Up @@ -112,10 +121,11 @@ public static StructuralViewModel Create(ApiStructuralPage page, ApiRenderContex
EmptyMessage = "This API does not declare servers."
};

internal static bool HasSchemes(OpenApiDocument document) => document.Components?.SecuritySchemes is { Count: > 0 };

internal static IReadOnlyList<AuthenticationSchemeDisplay> ReadSchemes(ApiRenderContext context)
{
var schemes = context.Model.Components?.SecuritySchemes;
if (schemes is not { Count: > 0 })
if (context.Model.Components?.SecuritySchemes is not { Count: > 0 } schemes)
return [];

var displays = new List<AuthenticationSchemeDisplay>(schemes.Count);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,33 +277,33 @@ public async Task Generate_WritesDistinctOutputTreesForMainAndReleasedMajors()
.File
.Exists(Path.Join(outputRoot, "api", "doc", "elasticsearch", "authentication", "index.html"))
.Should()
.BeTrue();
.BeFalse();
context
.WriteFileSystem
.File
.Exists(Path.Join(outputRoot, "api", "doc", "elasticsearch", "servers", "index.html"))
.Should()
.BeTrue();
.BeFalse();
context.WriteFileSystem.File.Exists(Path.Join(outputRoot, "api", "doc", "elasticsearch", "v9", "index.html")).Should().BeTrue();
context
.WriteFileSystem
.File
.Exists(Path.Join(outputRoot, "api", "doc", "elasticsearch", "v9", "authentication", "index.html"))
.Should()
.BeTrue();
.BeFalse();
context.WriteFileSystem.File.Exists(Path.Join(outputRoot, "api", "doc", "elasticsearch", "v8", "index.html")).Should().BeTrue();
context
.WriteFileSystem
.File
.Exists(Path.Join(outputRoot, "api", "doc", "elasticsearch", "v8", "authentication", "index.html"))
.Should()
.BeTrue();
.BeFalse();
context
.WriteFileSystem
.File
.Exists(Path.Join(outputRoot, "api", "doc", "elasticsearch", "v8", "servers", "index.html"))
.Should()
.BeTrue();
.BeFalse();
context
.WriteFileSystem
.File
Expand Down
54 changes: 54 additions & 0 deletions tests/Elastic.ApiExplorer.Tests/StructuralPageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using AwesomeAssertions;
using Elastic.ApiExplorer.Infrastructure;
using Elastic.ApiExplorer.Landing;
using Elastic.ApiExplorer.Structural;
using Elastic.Documentation.Navigation;
using Elastic.Documentation.Site.FileProviders;
Expand Down Expand Up @@ -35,6 +36,59 @@ public void CreateNavigation_IncludesAuthenticationAndServersPages()
servers.NavigationTitle.Should().Be("Servers");
}

[Fact]
public void Create_EmptyDocument_OmitsAuthenticationAndServersPages()
{
var items = CreateItems(new OpenApiDocument { Info = new OpenApiInfo { Title = "t", Version = "1" } });

items.Should().BeEmpty();
}

[Fact]
public void Create_NoSchemes_OmitsAuthenticationPage()
{
var document = new OpenApiDocument
{
Info = new OpenApiInfo { Title = "t", Version = "1" },
Servers = [new OpenApiServer { Url = "https://example.com" }]
};
var items = CreateItems(document);

items.Should().ContainSingle(item => item.Model.Kind == ApiStructuralKind.Servers);
items.Should().NotContain(item => item.Model.Kind == ApiStructuralKind.Authentication);
}

[Fact]
public void Create_NoServers_OmitsServersPage()
{
var document = new OpenApiDocument
{
Info = new OpenApiInfo { Title = "t", Version = "1" },
Components = new OpenApiComponents
{
SecuritySchemes = new Dictionary<string, IOpenApiSecurityScheme>
{
["apiKey"] = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.ApiKey,
Name = "Authorization",
In = ParameterLocation.Header
}
}
}
};
var items = CreateItems(document);

items.Should().ContainSingle(item => item.Model.Kind == ApiStructuralKind.Authentication);
items.Should().NotContain(item => item.Model.Kind == ApiStructuralKind.Servers);
}

private static IReadOnlyList<StructuralNavigationItem> CreateItems(OpenApiDocument document)
{
var root = new LandingNavigationItem("/api/doc/fixture");
return StructuralNavigationItem.Create(urlPathPrefix: null, "fixture", root, document);
}

[Fact]
public void ReadSchemes_MapsFixtureApiKey()
{
Expand Down
Loading