diff --git a/Directory.Packages.props b/Directory.Packages.props index 7ddf27714..4a9060035 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -42,7 +42,7 @@ - + diff --git a/docs/_docset.yml b/docs/_docset.yml index a09f0def2..8ab198efe 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -20,8 +20,9 @@ subs: features: primary-nav: false -#api: kibana-openapi.json -api: elasticsearch-openapi.json +api: + elasticsearch: elasticsearch-openapi.json + kibana: kibana-openapi.json toc: - file: index.md diff --git a/src/Elastic.ApiExplorer/OpenApiGenerator.cs b/src/Elastic.ApiExplorer/OpenApiGenerator.cs index d129ee0bb..6beae60d8 100644 --- a/src/Elastic.ApiExplorer/OpenApiGenerator.cs +++ b/src/Elastic.ApiExplorer/OpenApiGenerator.cs @@ -44,9 +44,9 @@ public class OpenApiGenerator(ILoggerFactory logFactory, BuildContext context, I private readonly IFileSystem _writeFileSystem = context.WriteFileSystem; private readonly StaticFileContentHashProvider _contentHashProvider = new(new EmbeddedOrPhysicalFileProvider(context)); - public LandingNavigationItem CreateNavigation(OpenApiDocument openApiDocument) + public LandingNavigationItem CreateNavigation(string apiUrlSuffix, OpenApiDocument openApiDocument) { - var url = $"{context.UrlPathPrefix}/api"; + var url = $"{context.UrlPathPrefix}/api/" + apiUrlSuffix; var rootNavigation = new LandingNavigationItem(url); var ops = openApiDocument.Paths @@ -62,15 +62,17 @@ public LandingNavigationItem CreateNavigation(OpenApiDocument openApiDocument) ? anyApi.Node.GetValue() : null; var tag = op.Value.Tags?.FirstOrDefault()?.Reference.Id; - var classification = openApiDocument.Info.Title == "Elasticsearch Request & Response Specification" - ? ClassifyElasticsearchTag(tag ?? "unknown") - : "unknown"; + var tagClassification = (extensions?.TryGetValue("x-tag-group", out var g) ?? false) && g is OpenApiAny anyTagGroup + ? anyTagGroup.Node.GetValue() + : openApiDocument.Info.Title == "Elasticsearch Request & Response Specification" + ? ClassifyElasticsearchTag(tag ?? "unknown") + : "unknown"; var apiString = ns is null ? api ?? op.Value.Summary ?? Guid.NewGuid().ToString("N") : $"{ns}.{api}"; return new { - Classification = classification, + Classification = tagClassification, Api = apiString, Tag = tag, pair.Path, @@ -158,25 +160,26 @@ group tagGroup by classificationGroup.Key var hasClassifications = classifications.Count > 1; foreach (var classification in classifications) { - if (hasClassifications) + if (hasClassifications && classification.Name != "common") { var classificationNavigationItem = new ClassificationNavigationItem(classification, rootNavigation, rootNavigation); var tagNavigationItems = new List>(); - CreateTagNavigationItems(classification, classificationNavigationItem, classificationNavigationItem, tagNavigationItems); + CreateTagNavigationItems(apiUrlSuffix, classification, classificationNavigationItem, classificationNavigationItem, tagNavigationItems); topLevelNavigationItems.Add(classificationNavigationItem); // if there is only a single tag item will be added directly to the classificationNavigationItem, otherwise they will be added to the tagNavigationItems if (classificationNavigationItem.NavigationItems.Count == 0) classificationNavigationItem.NavigationItems = tagNavigationItems; } else - CreateTagNavigationItems(classification, rootNavigation, rootNavigation, topLevelNavigationItems); + CreateTagNavigationItems(apiUrlSuffix, classification, rootNavigation, rootNavigation, topLevelNavigationItems); } rootNavigation.NavigationItems = topLevelNavigationItems; return rootNavigation; } private void CreateTagNavigationItems( + string apiUrlSuffix, ApiClassification classification, IRootNavigationItem rootNavigation, IApiGroupingNavigationItem parent, @@ -190,13 +193,13 @@ List> parentNavig if (hasTags) { var tagNavigationItem = new TagNavigationItem(tag, rootNavigation, parent); - CreateEndpointNavigationItems(rootNavigation, tag, tagNavigationItem, endpointNavigationItems); + CreateEndpointNavigationItems(apiUrlSuffix, rootNavigation, tag, tagNavigationItem, endpointNavigationItems); parentNavigationItems.Add(tagNavigationItem); tagNavigationItem.NavigationItems = endpointNavigationItems; } else { - CreateEndpointNavigationItems(rootNavigation, tag, parent, endpointNavigationItems); + CreateEndpointNavigationItems(apiUrlSuffix, rootNavigation, tag, parent, endpointNavigationItems); if (parent is ClassificationNavigationItem classificationNavigationItem) classificationNavigationItem.NavigationItems = endpointNavigationItems; else if (parent is LandingNavigationItem landingNavigationItem) @@ -206,6 +209,7 @@ List> parentNavig } private void CreateEndpointNavigationItems( + string apiUrlSuffix, IRootNavigationItem rootNavigation, ApiTag tag, IApiGroupingNavigationItem parentNavigationItem, @@ -220,7 +224,7 @@ List endpointNavigationItems var operationNavigationItems = new List(); foreach (var operation in endpoint.Operations) { - var operationNavigationItem = new OperationNavigationItem(context.UrlPathPrefix, operation, rootNavigation, endpointNavigationItem) + var operationNavigationItem = new OperationNavigationItem(context.UrlPathPrefix, apiUrlSuffix, operation, rootNavigation, endpointNavigationItem) { Hidden = true }; @@ -232,7 +236,7 @@ List endpointNavigationItems else { var operation = endpoint.Operations.First(); - var operationNavigationItem = new OperationNavigationItem(context.UrlPathPrefix, operation, rootNavigation, parentNavigationItem); + var operationNavigationItem = new OperationNavigationItem(context.UrlPathPrefix, apiUrlSuffix, operation, rootNavigation, parentNavigationItem); endpointNavigationItems.Add(operationNavigationItem); } @@ -241,47 +245,53 @@ List endpointNavigationItems public async Task Generate(Cancel ctx = default) { - if (context.Configuration.OpenApiSpecification is null) + if (context.Configuration.OpenApiSpecifications is null) return; - var openApiDocument = await OpenApiReader.Create(context.Configuration.OpenApiSpecification); - if (openApiDocument is null) - return; + foreach (var (prefix, path) in context.Configuration.OpenApiSpecifications) + { + var openApiDocument = await OpenApiReader.Create(path); + if (openApiDocument is null) + return; - var navigation = CreateNavigation(openApiDocument); - _logger.LogInformation("Generating OpenApiDocument {Title}", openApiDocument.Info.Title); + var navigation = CreateNavigation(prefix, openApiDocument); + _logger.LogInformation("Generating OpenApiDocument {Title}", openApiDocument.Info.Title); - var navigationRenderer = new IsolatedBuildNavigationHtmlWriter(context, navigation); + var navigationRenderer = new IsolatedBuildNavigationHtmlWriter(context, navigation); + var renderContext = new ApiRenderContext(context, openApiDocument, _contentHashProvider) + { + NavigationHtml = string.Empty, + CurrentNavigation = navigation, + MarkdownRenderer = markdownStringRenderer + }; + _ = await Render(prefix, navigation, navigation.Index, renderContext, navigationRenderer, ctx); + await RenderNavigationItems(prefix, renderContext, navigationRenderer, navigation, ctx); - var renderContext = new ApiRenderContext(context, openApiDocument, _contentHashProvider) - { - NavigationHtml = string.Empty, - CurrentNavigation = navigation, - MarkdownRenderer = markdownStringRenderer - }; - _ = await Render(navigation, navigation.Index, renderContext, navigationRenderer, ctx); - await RenderNavigationItems(navigation); + } + } - async Task RenderNavigationItems(INavigationItem currentNavigation) + private async Task RenderNavigationItems(string prefix, ApiRenderContext renderContext, IsolatedBuildNavigationHtmlWriter navigationRenderer, INavigationItem currentNavigation, Cancel ctx) + { + if (currentNavigation is INodeNavigationItem node) { - if (currentNavigation is INodeNavigationItem node) - { - _ = await Render(node, node.Index, renderContext, navigationRenderer, ctx); - foreach (var child in node.NavigationItems) - await RenderNavigationItems(child); - } + _ = await Render(prefix, node, node.Index, renderContext, navigationRenderer, ctx); + foreach (var child in node.NavigationItems) + await RenderNavigationItems(prefix, renderContext, navigationRenderer, child, ctx); + } -#pragma warning disable IDE0045 - else if (currentNavigation is ILeafNavigationItem leaf) -#pragma warning restore IDE0045 - _ = await Render(leaf, leaf.Model, renderContext, navigationRenderer, ctx); - else - throw new Exception($"Unknown navigation item type {currentNavigation.GetType()}"); + else + { + _ = currentNavigation is ILeafNavigationItem leaf + ? await Render(prefix, leaf, leaf.Model, renderContext, navigationRenderer, ctx) + : throw new Exception($"Unknown navigation item type {currentNavigation.GetType()}"); } } - private async Task Render(INavigationItem current, T page, ApiRenderContext renderContext, IsolatedBuildNavigationHtmlWriter navigationRenderer, Cancel ctx) +#pragma warning disable IDE0060 + private async Task Render(string prefix, INavigationItem current, T page, ApiRenderContext renderContext, +#pragma warning restore IDE0060 + IsolatedBuildNavigationHtmlWriter navigationRenderer, Cancel ctx) where T : INavigationModel, IPageRenderer { var outputFile = OutputFile(current); diff --git a/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs b/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs index 6e62145f7..1039603f9 100644 --- a/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs +++ b/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs @@ -12,6 +12,19 @@ namespace Elastic.ApiExplorer.Operations; +public interface IApiProperty +{ + +} + +public record ApiObject +{ + public required string Name { get; init; } + public IReadOnlyCollection Properties { get; init; } = []; +} + + + public record ApiOperation(OperationType OperationType, OpenApiOperation Operation, string Route, IOpenApiPathItem Path, string ApiName) : IApiModel { public async Task RenderAsync(FileSystemStream stream, ApiRenderContext context, Cancel ctx = default) @@ -29,6 +42,7 @@ public class OperationNavigationItem : ILeafNavigationItem, IEndpo { public OperationNavigationItem( string? urlPathPrefix, + string apiUrlSuffix, ApiOperation apiOperation, IRootNavigationItem root, IApiGroupingNavigationItem parent @@ -39,7 +53,7 @@ IApiGroupingNavigationItem parent NavigationTitle = apiOperation.ApiName; Parent = parent; var moniker = apiOperation.Operation.OperationId ?? apiOperation.Route.Replace("}", "").Replace("{", "").Replace('/', '-'); - Url = $"{urlPathPrefix}/api/endpoints/{moniker}"; + Url = $"{urlPathPrefix?.TrimEnd('/')}/api/{apiUrlSuffix}/{moniker}"; Id = ShortId.Create(Url); } diff --git a/src/Elastic.ApiExplorer/Operations/OperationView.cshtml b/src/Elastic.ApiExplorer/Operations/OperationView.cshtml index 0c9977f23..198641c48 100644 --- a/src/Elastic.ApiExplorer/Operations/OperationView.cshtml +++ b/src/Elastic.ApiExplorer/Operations/OperationView.cshtml @@ -1,10 +1,59 @@ @using Elastic.ApiExplorer.Landing @using Elastic.ApiExplorer.Operations @using Microsoft.OpenApi.Models +@using Microsoft.OpenApi.Models.Interfaces @inherits RazorSliceHttpResult @implements IUsesLayout @functions { public GlobalLayoutViewModel LayoutModel => Model.CreateGlobalLayoutModel(); + + public string GetTypeName(JsonSchemaType? type) + { + var typeName = ""; + if (type is null) + return "unknown and null"; + + if (type.Value.HasFlag(JsonSchemaType.Boolean)) + typeName = "boolean"; + else if (type.Value.HasFlag(JsonSchemaType.Integer)) + typeName = "integer"; + else if (type.Value.HasFlag(JsonSchemaType.String)) + typeName = "string"; + else if (type.Value.HasFlag(JsonSchemaType.Object)) + { + typeName = "object"; + } + else if (type.Value.HasFlag(JsonSchemaType.Null)) + typeName = "null"; + else if (type.Value.HasFlag(JsonSchemaType.Number)) + typeName = "number"; + else + { + } + + if (type.Value.HasFlag(JsonSchemaType.Array)) + typeName += " array"; + return typeName; + } + + public string GetTypeName(IOpenApiSchema propertyValue) + { + var typeName = string.Empty; + if (propertyValue.Type is not null) + { + typeName = GetTypeName(propertyValue.Type); + if (typeName is not "object" and not "array") + return typeName; + } + + if (propertyValue.Schema is not null) + return propertyValue.Schema; + + if (propertyValue.Enum is { Count: >0 } e) + return "enum"; + + return $"unknown value {typeName}"; + } } @{ var self = Model.CurrentNavigationItem as OperationNavigationItem; @@ -34,7 +83,7 @@ var method = overload.Model.OperationType.ToString().ToLowerInvariant(); var current = overload.Model.Route == Model.Operation.Route && overload.Model.OperationType == Model.Operation.OperationType ? "current" : "";
  • - + @method.ToUpperInvariant() @overload.Model.Route @@ -72,15 +121,26 @@ @if (operation.RequestBody is not null) {

    Request Body

    + var content = operation.RequestBody.Content.FirstOrDefault().Value; if (!string.IsNullOrEmpty(operation.RequestBody.Description)) {

    @operation.RequestBody.Description

    } + + if (content.Schema is not null) + {
    - @foreach (var path in operation.RequestBody.Content) + @foreach (var property in content.Schema.Properties) { + if (property.Value.Type is null) + { + + } +
    @property.Key @GetTypeName(property.Value)
    +
    @Model.RenderMarkdown(property.Value.Description)
    }
    + } }