Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<PackageVersion Include="Markdig" Version="0.41.1" />
<PackageVersion Include="NetEscapades.EnumGenerators" Version="1.0.0-beta12" PrivateAssets="all" ExcludeAssets="runtime" />
<PackageVersion Include="Proc" Version="0.9.1" />
<PackageVersion Include="RazorSlices" Version="0.9.0" />
<PackageVersion Include="RazorSlices" Version="0.9.2" />
<PackageVersion Include="Samboy063.Tomlet" Version="6.0.0" />
<PackageVersion Include="Slugify.Core" Version="4.0.1" />
<PackageVersion Include="SoftCircuits.IniFileParser" Version="2.7.0" />
Expand Down
5 changes: 3 additions & 2 deletions docs/_docset.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
94 changes: 52 additions & 42 deletions src/Elastic.ApiExplorer/OpenApiGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -62,15 +62,17 @@ public LandingNavigationItem CreateNavigation(OpenApiDocument openApiDocument)
? anyApi.Node.GetValue<string>()
: 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<string>()
: 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,
Expand Down Expand Up @@ -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<IApiGroupingNavigationItem<IApiGroupingModel, INavigationItem>>();

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<IApiGroupingModel, INavigationItem> rootNavigation,
IApiGroupingNavigationItem<IApiGroupingModel, INavigationItem> parent,
Expand All @@ -190,13 +193,13 @@ List<IApiGroupingNavigationItem<IApiGroupingModel, INavigationItem>> 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)
Expand All @@ -206,6 +209,7 @@ List<IApiGroupingNavigationItem<IApiGroupingModel, INavigationItem>> parentNavig
}

private void CreateEndpointNavigationItems(
string apiUrlSuffix,
IRootNavigationItem<IApiGroupingModel, INavigationItem> rootNavigation,
ApiTag tag,
IApiGroupingNavigationItem<IApiGroupingModel, INavigationItem> parentNavigationItem,
Expand All @@ -220,7 +224,7 @@ List<IEndpointOrOperationNavigationItem> endpointNavigationItems
var operationNavigationItems = new List<OperationNavigationItem>();
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
};
Expand All @@ -232,7 +236,7 @@ List<IEndpointOrOperationNavigationItem> 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);

}
Expand All @@ -241,47 +245,53 @@ List<IEndpointOrOperationNavigationItem> 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<IApiModel, INavigationItem> node)
{
if (currentNavigation is INodeNavigationItem<IApiModel, INavigationItem> 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<IApiModel> 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<IApiModel> leaf
? await Render(prefix, leaf, leaf.Model, renderContext, navigationRenderer, ctx)
: throw new Exception($"Unknown navigation item type {currentNavigation.GetType()}");
}
}

private async Task<IFileInfo> Render<T>(INavigationItem current, T page, ApiRenderContext renderContext, IsolatedBuildNavigationHtmlWriter navigationRenderer, Cancel ctx)
#pragma warning disable IDE0060
private async Task<IFileInfo> Render<T>(string prefix, INavigationItem current, T page, ApiRenderContext renderContext,
#pragma warning restore IDE0060
IsolatedBuildNavigationHtmlWriter navigationRenderer, Cancel ctx)
where T : INavigationModel, IPageRenderer<ApiRenderContext>
{
var outputFile = OutputFile(current);
Expand Down
16 changes: 15 additions & 1 deletion src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@

namespace Elastic.ApiExplorer.Operations;

public interface IApiProperty
{

}

public record ApiObject
{
public required string Name { get; init; }
public IReadOnlyCollection<IApiProperty> 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)
Expand All @@ -29,6 +42,7 @@ public class OperationNavigationItem : ILeafNavigationItem<ApiOperation>, IEndpo
{
public OperationNavigationItem(
string? urlPathPrefix,
string apiUrlSuffix,
ApiOperation apiOperation,
IRootNavigationItem<IApiGroupingModel, INavigationItem> root,
IApiGroupingNavigationItem<IApiGroupingModel, INavigationItem> parent
Expand All @@ -39,7 +53,7 @@ IApiGroupingNavigationItem<IApiGroupingModel, INavigationItem> 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);
}

Expand Down
64 changes: 62 additions & 2 deletions src/Elastic.ApiExplorer/Operations/OperationView.cshtml
Original file line number Diff line number Diff line change
@@ -1,10 +1,59 @@
@using Elastic.ApiExplorer.Landing
@using Elastic.ApiExplorer.Operations
@using Microsoft.OpenApi.Models
@using Microsoft.OpenApi.Models.Interfaces
@inherits RazorSliceHttpResult<Elastic.ApiExplorer.Operations.OperationViewModel>
@implements IUsesLayout<Elastic.ApiExplorer._Layout, GlobalLayoutViewModel>
@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;
Expand Down Expand Up @@ -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" : "";
<li class="api-url-list-item">
<a href="@overload.Url" class="@current">
<a href="@overload.Url" class="@current" hx-disable="true">
<span class="api-method api-method-@method">@method.ToUpperInvariant()</span>
<span class="api-url">@overload.Model.Route</span>
</a>
Expand Down Expand Up @@ -72,15 +121,26 @@
@if (operation.RequestBody is not null)
{
<h3>Request Body</h3>
var content = operation.RequestBody.Content.FirstOrDefault().Value;
if (!string.IsNullOrEmpty(operation.RequestBody.Description))
{
<p>@operation.RequestBody.Description</p>
}

if (content.Schema is not null)
{
<dl>
@foreach (var path in operation.RequestBody.Content)
@foreach (var property in content.Schema.Properties)
{
if (property.Value.Type is null)
{

}
<dt id="@property.Key"><a href="#@property.Key"><code>@property.Key</code> @GetTypeName(property.Value) </a></dt>
<dd>@Model.RenderMarkdown(property.Value.Description)</dd>
}
</dl>
}
}
</section>
<aside>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public record ConfigurationFile : ITableOfContentsScope

public IDirectoryInfo ScopeDirectory { get; }

public IFileInfo? OpenApiSpecification { get; }
public IReadOnlyDictionary<string, IFileInfo>? OpenApiSpecifications { get; }

/// This is a documentation set that is not linked to by assembler.
/// Setting this to true relaxes a few restrictions such as mixing toc references with file and folder reference
Expand Down Expand Up @@ -112,11 +112,18 @@ public ConfigurationFile(IDocumentationContext context, VersionsConfiguration ve
// read this later
break;
case "api":
var specification = reader.ReadString(entry.Entry);
if (specification is null)
var configuredApis = reader.ReadDictionary(entry.Entry);
if (configuredApis.Count == 0)
break;
var path = Path.Combine(context.DocumentationSourceDirectory.FullName, specification);
OpenApiSpecification = context.ReadFileSystem.FileInfo.New(path);

var specs = new Dictionary<string, IFileInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var (k, v) in configuredApis)
{
var path = Path.Combine(context.DocumentationSourceDirectory.FullName, v);
var fi = context.ReadFileSystem.FileInfo.New(path);
specs[k] = fi;
}
OpenApiSpecifications = specs;
break;
case "products":
if (entry.Entry.Value is not YamlSequenceNode sequence)
Expand All @@ -130,7 +137,7 @@ public ConfigurationFile(IDocumentationContext context, VersionsConfiguration ve
YamlScalarNode? productId = null;
foreach (var child in node.Children)
{
if (child.Key is YamlScalarNode { Value: "id" } && child.Value is YamlScalarNode scalarNode)
if (child is { Key: YamlScalarNode { Value: "id" }, Value: YamlScalarNode scalarNode })
{
productId = scalarNode;
break;
Expand Down
3 changes: 2 additions & 1 deletion src/tooling/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<!-- TODO ENABLE to document our code properly <GenerateDocumentationFile>true</GenerateDocumentationFile> -->
<PublishRepositoryUrl>true</PublishRepositoryUrl>

</PropertyGroup>

<ItemGroup Condition="'$(OutputType)' == 'Exe'">
Expand All @@ -17,4 +18,4 @@
<Content Include="$(SolutionRoot)\NOTICE.txt" Pack="True" PackagePath="NOTICE.txt" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest"/>
</ItemGroup>

</Project>
</Project>
Loading
Loading