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
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