Skip to content

Commit 1873a55

Browse files
authored
[API Explorer] Landing page setup (#3120)
1 parent f3884a2 commit 1873a55

8 files changed

Lines changed: 544 additions & 12 deletions

File tree

src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ public record ConfigurationFile
5858

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

61+
/// <summary>
62+
/// Resolved API configurations with template and specification file information.
63+
/// </summary>
64+
public IReadOnlyDictionary<string, ResolvedApiConfiguration>? ApiConfigurations { get; }
65+
6166
/// <summary>
6267
/// Set of diagnostic hint types to suppress for this documentation set.
6368
/// </summary>
@@ -134,17 +139,81 @@ public ConfigurationFile(DocumentationSetFile docSetFile, IDocumentationSetConte
134139
// Read substitutions
135140
_substitutions = new(docSetFile.Subs, StringComparer.OrdinalIgnoreCase);
136141

137-
// Process API specifications
142+
// Process API configurations
138143
if (docSetFile.Api.Count > 0)
139144
{
140145
var specs = new Dictionary<string, IFileInfo>(StringComparer.OrdinalIgnoreCase);
141-
foreach (var (k, v) in docSetFile.Api)
146+
var apiConfigs = new Dictionary<string, ResolvedApiConfiguration>(StringComparer.OrdinalIgnoreCase);
147+
148+
foreach (var (productKey, apiConfig) in docSetFile.Api)
142149
{
143-
var path = Path.Join(context.DocumentationSourceDirectory.FullName, v);
144-
var fi = context.ReadFileSystem.FileInfo.New(path);
145-
specs[k] = fi;
150+
if (!apiConfig.IsValid)
151+
{
152+
context.EmitError(
153+
context.ConfigurationPath,
154+
$"API configuration for '{productKey}' is invalid. Must have at least one spec and cannot specify both 'spec' and 'specs'."
155+
);
156+
continue;
157+
}
158+
159+
// Resolve template file if specified
160+
IFileInfo? templateFile = null;
161+
if (!string.IsNullOrEmpty(apiConfig.Template))
162+
{
163+
var templatePath = Path.Join(context.DocumentationSourceDirectory.FullName, apiConfig.Template);
164+
templateFile = context.ReadFileSystem.FileInfo.New(templatePath);
165+
if (!templateFile.Exists)
166+
{
167+
context.EmitWarning(
168+
context.ConfigurationPath,
169+
$"Template file '{apiConfig.Template}' for API '{productKey}' does not exist."
170+
);
171+
templateFile = null;
172+
}
173+
}
174+
175+
// Resolve specification files
176+
var specFiles = new List<IFileInfo>();
177+
foreach (var specPath in apiConfig.GetSpecPaths())
178+
{
179+
var fullPath = Path.Join(context.DocumentationSourceDirectory.FullName, specPath);
180+
var specFile = context.ReadFileSystem.FileInfo.New(fullPath);
181+
if (!specFile.Exists)
182+
{
183+
context.EmitError(
184+
context.ConfigurationPath,
185+
$"API specification file '{specPath}' for product '{productKey}' does not exist."
186+
);
187+
continue;
188+
}
189+
specFiles.Add(specFile);
190+
}
191+
192+
if (specFiles.Count == 0)
193+
{
194+
context.EmitError(
195+
context.ConfigurationPath,
196+
$"No valid specification files found for API product '{productKey}'."
197+
);
198+
continue;
199+
}
200+
201+
// Create resolved configuration
202+
var resolvedConfig = new ResolvedApiConfiguration
203+
{
204+
ProductKey = productKey,
205+
TemplateFile = templateFile,
206+
SpecFiles = specFiles
207+
};
208+
209+
apiConfigs[productKey] = resolvedConfig;
210+
211+
// For backward compatibility, populate OpenApiSpecifications with primary spec
212+
specs[productKey] = resolvedConfig.PrimarySpecFile;
146213
}
147-
OpenApiSpecifications = specs;
214+
215+
OpenApiSpecifications = specs.Count > 0 ? specs : null;
216+
ApiConfigurations = apiConfigs.Count > 0 ? apiConfigs : null;
148217
}
149218

150219
// Process products from docset - resolve ProductLinks to Product objects

src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public partial class ConfigurationFileProvider
2828
.WithTypeConverter(new TocItemYamlConverter())
2929
.WithTypeConverter(new SiteTableOfContentsCollectionYamlConverter())
3030
.WithTypeConverter(new SiteTableOfContentsRefYamlConverter())
31+
.WithTypeConverter(new ApiConfigurationConverter())
3132
.Build();
3233

3334
public ConfigurationSource ConfigurationSource { get; }
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.IO.Abstractions;
6+
using YamlDotNet.Serialization;
7+
8+
namespace Elastic.Documentation.Configuration.Toc;
9+
10+
/// <summary>
11+
/// Configuration for API documentation generation from OpenAPI specifications.
12+
/// </summary>
13+
[YamlSerializable]
14+
public class ApiConfiguration
15+
{
16+
/// <summary>
17+
/// Path to a single OpenAPI specification file (for backward compatibility).
18+
/// Cannot be used together with <see cref="Specs"/>.
19+
/// </summary>
20+
[YamlMember(Alias = "spec")]
21+
public string? Spec { get; set; }
22+
23+
/// <summary>
24+
/// Paths to multiple OpenAPI specification files (future feature).
25+
/// Cannot be used together with <see cref="Spec"/>.
26+
/// </summary>
27+
[YamlMember(Alias = "specs")]
28+
public List<string>? Specs { get; set; }
29+
30+
/// <summary>
31+
/// Path to a Markdown template file to use as the API landing page.
32+
/// If not specified, an auto-generated landing page will be used.
33+
/// </summary>
34+
[YamlMember(Alias = "template")]
35+
public string? Template { get; set; }
36+
37+
/// <summary>
38+
/// Validates that the configuration is valid.
39+
/// Must have a non-empty spec path. Multi-spec support is deferred to future implementation.
40+
/// </summary>
41+
public bool IsValid =>
42+
!string.IsNullOrWhiteSpace(Spec);
43+
44+
/// <summary>
45+
/// Gets all specification file paths, handling both single spec and multi-spec configurations.
46+
/// </summary>
47+
public IEnumerable<string> GetSpecPaths()
48+
{
49+
if (Spec != null)
50+
yield return Spec;
51+
52+
if (Specs?.Count > 0)
53+
{
54+
foreach (var spec in Specs)
55+
yield return spec;
56+
}
57+
}
58+
}
59+
60+
/// <summary>
61+
/// Resolved API configuration with validated file references.
62+
/// </summary>
63+
public class ResolvedApiConfiguration
64+
{
65+
public required string ProductKey { get; init; }
66+
public IFileInfo? TemplateFile { get; init; }
67+
public required List<IFileInfo> SpecFiles { get; init; }
68+
69+
/// <summary>
70+
/// Whether this configuration has a custom template file.
71+
/// </summary>
72+
public bool HasCustomTemplate => TemplateFile != null;
73+
74+
/// <summary>
75+
/// Primary specification file (first in the list, for backward compatibility).
76+
/// </summary>
77+
public IFileInfo PrimarySpecFile => SpecFiles.First();
78+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using YamlDotNet.Core;
6+
using YamlDotNet.Core.Events;
7+
using YamlDotNet.Serialization;
8+
using static YamlDotNet.Core.ParserExtensions;
9+
10+
namespace Elastic.Documentation.Configuration.Toc;
11+
12+
/// <summary>
13+
/// YAML converter that provides backward compatibility for API configuration.
14+
/// Supports both old string format and new object format.
15+
///
16+
/// Old format: api: { elasticsearch: "elasticsearch-openapi.json" }
17+
/// New format: api: { elasticsearch: { spec: "elasticsearch-openapi.json", template: "elasticsearch-api-overview.md" } }
18+
/// </summary>
19+
public class ApiConfigurationConverter : IYamlTypeConverter
20+
{
21+
public bool Accepts(Type type) => type == typeof(ApiConfiguration);
22+
23+
public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
24+
{
25+
if (parser.Current is Scalar scalar)
26+
{
27+
// Handle old string format: "elasticsearch-openapi.json"
28+
_ = parser.MoveNext();
29+
return new ApiConfiguration { Spec = scalar.Value };
30+
}
31+
32+
if (parser.Current is MappingStart)
33+
{
34+
// Handle new object format: { spec: "...", template: "...", specs: [...] }
35+
_ = parser.MoveNext();
36+
var config = new ApiConfiguration();
37+
38+
while (parser.Current is not MappingEnd)
39+
{
40+
var key = parser.Consume<Scalar>();
41+
switch (key.Value)
42+
{
43+
case "spec":
44+
if (parser.Current is Scalar specValue)
45+
{
46+
config.Spec = specValue.Value;
47+
_ = parser.MoveNext();
48+
}
49+
else
50+
{
51+
// Wrong token type - skip safely
52+
parser.SkipThisAndNestedEvents();
53+
}
54+
break;
55+
case "template":
56+
if (parser.Current is Scalar templateValue)
57+
{
58+
config.Template = templateValue.Value;
59+
_ = parser.MoveNext();
60+
}
61+
else
62+
{
63+
// Wrong token type - skip safely
64+
parser.SkipThisAndNestedEvents();
65+
}
66+
break;
67+
default:
68+
// Safely consume unknown values (including nested mappings/sequences)
69+
parser.SkipThisAndNestedEvents();
70+
break;
71+
}
72+
}
73+
74+
_ = parser.MoveNext(); // consume MappingEnd
75+
return config;
76+
}
77+
78+
throw new YamlException(parser.Current?.Start ?? Mark.Empty, parser.Current?.End ?? Mark.Empty,
79+
"API configuration must be either a string (spec path) or an object with spec/template fields");
80+
}
81+
82+
public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer)
83+
{
84+
if (value is ApiConfiguration config)
85+
{
86+
// Always write as object format for consistency
87+
serializer(config, typeof(ApiConfiguration));
88+
}
89+
}
90+
}

src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public class DocumentationSetFile : TableOfContentsFile
5252
public DocumentationSetFeatures Features { get; set; } = new();
5353

5454
[YamlMember(Alias = "api")]
55-
public Dictionary<string, string> Api { get; set; } = [];
55+
public Dictionary<string, ApiConfiguration> Api { get; set; } = [];
5656

5757
/// <summary>
5858
/// Default products for this documentation set. These are merged with page-level frontmatter products.

0 commit comments

Comments
 (0)