diff --git a/docs/source/markup/applies.md b/docs/source/markup/applies.md new file mode 100644 index 000000000..e8818e2ab --- /dev/null +++ b/docs/source/markup/applies.md @@ -0,0 +1,48 @@ +--- +title: Product Availability +applies: + stack: ga 8.1 + serverless: tech-preview + hosted: beta 8.1.1 + eck: beta 3.0.2 + ece: unavailable +--- + + +Using yaml frontmatter pages can explicitly indicate to each deployment targets availability and lifecycle status + + +```yaml +applies: + stack: ga 8.1 + serverless: tech-preview + hosted: beta 8.1.1 + eck: beta 3.0.2 + ece: unavailable +``` + +Its syntax is + +``` + : [version] +``` + +Where version is optional. + +`all` and empty string mean generally available for all active versions + +```yaml +applies: + stack: + serverless: all +``` + +`all` and empty string can also be specified at a version level + +```yaml +applies: + stack: beta all + serverless: beta +``` + +Are equivalent, note `all` just means we won't be rendering the version portion in the html. \ No newline at end of file diff --git a/src/Elastic.Markdown/Helpers/SemVersion.cs b/src/Elastic.Markdown/Helpers/SemVersion.cs index d59f7caa4..e365257e7 100644 --- a/src/Elastic.Markdown/Helpers/SemVersion.cs +++ b/src/Elastic.Markdown/Helpers/SemVersion.cs @@ -11,7 +11,7 @@ namespace Elastic.Markdown.Helpers; /// /// A semver2 compatible version. /// -public sealed class SemVersion : +public class SemVersion : IEquatable, IComparable, IComparable @@ -92,6 +92,14 @@ public SemVersion(int major, int minor, int patch, string? prerelease, string? m Metadata = metadata ?? string.Empty; } + public static explicit operator SemVersion(string b) + { + var semVersion = TryParse(b, out var version) ? version : TryParse(b + ".0", out version) ? version : null; + return semVersion ?? throw new ArgumentException($"'{b}' is not a valid semver2 version string."); + } + + public static implicit operator string(SemVersion d) => d.ToString(); + /// /// /// diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index ece680f78..477b61943 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -5,6 +5,7 @@ using Elastic.Markdown.Diagnostics; using Elastic.Markdown.Myst; using Elastic.Markdown.Myst.Directives; +using Elastic.Markdown.Myst.FrontMatter; using Elastic.Markdown.Slices; using Markdig; using Markdig.Extensions.Yaml; diff --git a/src/Elastic.Markdown/Myst/Directives/IncludeBlock.cs b/src/Elastic.Markdown/Myst/Directives/IncludeBlock.cs index 974fb323d..28dd4c586 100644 --- a/src/Elastic.Markdown/Myst/Directives/IncludeBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/IncludeBlock.cs @@ -4,6 +4,7 @@ using System.IO.Abstractions; using Elastic.Markdown.Diagnostics; using Elastic.Markdown.IO; +using Elastic.Markdown.Myst.FrontMatter; namespace Elastic.Markdown.Myst.Directives; diff --git a/src/Elastic.Markdown/Myst/FrontMatter/AllVersions.cs b/src/Elastic.Markdown/Myst/FrontMatter/AllVersions.cs new file mode 100644 index 000000000..c61a59344 --- /dev/null +++ b/src/Elastic.Markdown/Myst/FrontMatter/AllVersions.cs @@ -0,0 +1,50 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Markdown.Helpers; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace Elastic.Markdown.Myst.FrontMatter; + +public class AllVersions() : SemVersion(9999, 9999, 9999) +{ + public static AllVersions Instance { get; } = new (); +} + +public class SemVersionConverter : IYamlTypeConverter +{ + public bool Accepts(Type type) => type == typeof(SemVersion); + + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + var value = parser.Consume(); + if (string.IsNullOrWhiteSpace(value.Value)) + return AllVersions.Instance; + if (string.Equals(value.Value.Trim(), "all", StringComparison.InvariantCultureIgnoreCase)) + return AllVersions.Instance; + return (SemVersion)value.Value; + } + + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) + { + if (value == null) + return; + emitter.Emit(new Scalar(value.ToString()!)); + } + + public static bool TryParse(string? value, out SemVersion? version) + { + version = value?.Trim().ToLowerInvariant() switch + { + null => AllVersions.Instance, + "all" => AllVersions.Instance, + "" => AllVersions.Instance, + _ => SemVersion.TryParse(value, out var v) ? v : SemVersion.TryParse(value + ".0", out v) ? v : null + }; + return version is not null; + } +} + diff --git a/src/Elastic.Markdown/Myst/FrontMatter/Deployment.cs b/src/Elastic.Markdown/Myst/FrontMatter/Deployment.cs new file mode 100644 index 000000000..e4916666a --- /dev/null +++ b/src/Elastic.Markdown/Myst/FrontMatter/Deployment.cs @@ -0,0 +1,120 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace Elastic.Markdown.Myst.FrontMatter; + +[YamlSerializable] +public record Deployment +{ + [YamlMember(Alias = "self")] + public SelfManagedDeployment? SelfManaged { get; set; } + + [YamlMember(Alias = "cloud")] + public CloudManagedDeployment? Cloud { get; set; } + + public static Deployment All { get; } = new() + { + Cloud = CloudManagedDeployment.All, + SelfManaged = SelfManagedDeployment.All + }; +} + +[YamlSerializable] +public record SelfManagedDeployment +{ + [YamlMember(Alias = "stack")] + public ProductAvailability? Stack { get; set; } + + [YamlMember(Alias = "ece")] + public ProductAvailability? Ece { get; set; } + + [YamlMember(Alias = "eck")] + public ProductAvailability? Eck { get; set; } + + public static SelfManagedDeployment All { get; } = new() + { + Stack = ProductAvailability.GenerallyAvailable, + Ece = ProductAvailability.GenerallyAvailable, + Eck = ProductAvailability.GenerallyAvailable + }; +} + +[YamlSerializable] +public record CloudManagedDeployment +{ + [YamlMember(Alias = "hosted")] + public ProductAvailability? Hosted { get; set; } + + [YamlMember(Alias = "serverless")] + public ProductAvailability? Serverless { get; set; } + + public static CloudManagedDeployment All { get; } = new() + { + Hosted = ProductAvailability.GenerallyAvailable, + Serverless = ProductAvailability.GenerallyAvailable + }; + +} + +public class DeploymentConverter : IYamlTypeConverter +{ + public bool Accepts(Type type) => type == typeof(Deployment); + + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + if (parser.TryConsume(out var value)) + { + if (string.IsNullOrWhiteSpace(value.Value)) + return Deployment.All; + if (string.Equals(value.Value, "all", StringComparison.InvariantCultureIgnoreCase)) + return Deployment.All; + } + var x = rootDeserializer.Invoke(typeof(Dictionary)); + if (x is not Dictionary { Count: > 0 } dictionary) + return null; + + var deployment = new Deployment(); + + if (TryGetVersion("stack", out var version)) + { + deployment.SelfManaged ??= new SelfManagedDeployment(); + deployment.SelfManaged.Stack = version; + } + if (TryGetVersion("ece", out version)) + { + deployment.SelfManaged ??= new SelfManagedDeployment(); + deployment.SelfManaged.Ece = version; + } + if (TryGetVersion("eck", out version)) + { + deployment.SelfManaged ??= new SelfManagedDeployment(); + deployment.SelfManaged.Eck = version; + } + if (TryGetVersion("hosted", out version)) + { + deployment.Cloud ??= new CloudManagedDeployment(); + deployment.Cloud.Hosted = version; + } + if (TryGetVersion("serverless", out version)) + { + deployment.Cloud ??= new CloudManagedDeployment(); + deployment.Cloud.Serverless = version; + } + return deployment; + + bool TryGetVersion(string key, out ProductAvailability? semVersion) + { + semVersion = null; + return dictionary.TryGetValue(key, out var v) && ProductAvailability.TryParse(v, out semVersion); + } + + } + + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => + serializer.Invoke(value, type); +} diff --git a/src/Elastic.Markdown/Myst/FrontMatterParser.cs b/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs similarity index 81% rename from src/Elastic.Markdown/Myst/FrontMatterParser.cs rename to src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs index 71efe322f..694045a1c 100644 --- a/src/Elastic.Markdown/Myst/FrontMatterParser.cs +++ b/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs @@ -1,10 +1,10 @@ // Licensed to Elasticsearch B.V under one or more agreements. // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information + using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; -namespace Elastic.Markdown.Myst; +namespace Elastic.Markdown.Myst.FrontMatter; [YamlStaticContext] public partial class YamlFrontMatterStaticContext; @@ -20,6 +20,10 @@ public class YamlFrontMatter [YamlMember(Alias = "sub")] public Dictionary? Properties { get; set; } + + + [YamlMember(Alias = "applies")] + public Deployment? AppliesTo { get; set; } } public static class FrontMatterParser @@ -30,6 +34,8 @@ public static YamlFrontMatter Deserialize(string yaml) var deserializer = new StaticDeserializerBuilder(new YamlFrontMatterStaticContext()) .IgnoreUnmatchedProperties() + .WithTypeConverter(new SemVersionConverter()) + .WithTypeConverter(new DeploymentConverter()) .Build(); var frontMatter = deserializer.Deserialize(input); @@ -37,3 +43,4 @@ public static YamlFrontMatter Deserialize(string yaml) } } + diff --git a/src/Elastic.Markdown/Myst/FrontMatter/ProductAvailability.cs b/src/Elastic.Markdown/Myst/FrontMatter/ProductAvailability.cs new file mode 100644 index 000000000..88336bc20 --- /dev/null +++ b/src/Elastic.Markdown/Myst/FrontMatter/ProductAvailability.cs @@ -0,0 +1,61 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Markdown.Helpers; +using YamlDotNet.Serialization; + +namespace Elastic.Markdown.Myst.FrontMatter; + +[YamlSerializable] +public record ProductAvailability +{ + public ProductLifecycle Lifecycle { get; init; } + public SemVersion? Version { get; init; } + + public static ProductAvailability GenerallyAvailable { get; } = new() + { + Lifecycle = ProductLifecycle.GenerallyAvailable, Version = AllVersions.Instance + }; + + // [version] + public static bool TryParse(string? value, out ProductAvailability? availability) + { + if (string.IsNullOrWhiteSpace(value) || string.Equals(value.Trim(), "all", StringComparison.InvariantCultureIgnoreCase)) + { + availability = GenerallyAvailable; + return true; + } + + var tokens = value.Split(" ", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (tokens.Length < 1) + { + availability = null; + return false; + } + var lifecycle = tokens[0].ToLowerInvariant() switch + { + "preview" => ProductLifecycle.TechnicalPreview, + "tech-preview" => ProductLifecycle.TechnicalPreview, + "beta" => ProductLifecycle.Beta, + "dev" => ProductLifecycle.Development, + "development" => ProductLifecycle.Development, + "deprecated" => ProductLifecycle.Deprecated, + "coming" => ProductLifecycle.Coming, + "discontinued" => ProductLifecycle.Discontinued, + "unavailable" => ProductLifecycle.Unavailable, + "ga" => ProductLifecycle.GenerallyAvailable, + _ => throw new ArgumentOutOfRangeException(nameof(tokens), tokens, $"Unknown product lifecycle: {tokens[0]}") + }; + + var version = tokens.Length < 2 ? null : tokens[1] switch + { + null => AllVersions.Instance, + "all" => AllVersions.Instance, + "" => AllVersions.Instance, + var t => SemVersionConverter.TryParse(t, out var v) ? v : null + }; + availability = new ProductAvailability { Version = version, Lifecycle = lifecycle }; + return true; + } +} diff --git a/src/Elastic.Markdown/Myst/FrontMatter/ProductLifecycle.cs b/src/Elastic.Markdown/Myst/FrontMatter/ProductLifecycle.cs new file mode 100644 index 000000000..8ce224fd5 --- /dev/null +++ b/src/Elastic.Markdown/Myst/FrontMatter/ProductLifecycle.cs @@ -0,0 +1,36 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using YamlDotNet.Serialization; + +namespace Elastic.Markdown.Myst.FrontMatter; + +[YamlSerializable] +public enum ProductLifecycle +{ + // technical preview (exists in current docs system per https://github.com/elastic/docs?tab=readme-ov-file#beta-dev-and-preview-experimental) + [YamlMember(Alias = "preview")] + TechnicalPreview, + // beta (ditto) + [YamlMember(Alias = "beta")] + Beta, + // dev (ditto, though it's uncertain whether it's ever used or still needed) + [YamlMember(Alias = "development")] + Development, + // deprecated (exists in current docs system per https://github.com/elastic/docs?tab=readme-ov-file#additions-and-deprecations) + [YamlMember(Alias = "deprecated")] + Deprecated, + // coming (ditto) + [YamlMember(Alias = "coming")] + Coming, + // discontinued (historically we've immediately removed content when the feature ceases to be supported, but this might not be the case with pages that contain information that spans versions) + [YamlMember(Alias = "discontinued")] + Discontinued, + // unavailable (for content that doesn't exist in a specific context and is never coming or not coming anytime soon) + [YamlMember(Alias = "unavailable")] + Unavailable, + // ga (replaces "added" in the current docs system since it was not entirely clear how/if that overlapped with beta/preview states) + [YamlMember(Alias = "ga")] + GenerallyAvailable +} diff --git a/src/Elastic.Markdown/Myst/MarkdownParser.cs b/src/Elastic.Markdown/Myst/MarkdownParser.cs index 925af0d13..9fccbc7c8 100644 --- a/src/Elastic.Markdown/Myst/MarkdownParser.cs +++ b/src/Elastic.Markdown/Myst/MarkdownParser.cs @@ -7,6 +7,7 @@ using Elastic.Markdown.IO; using Elastic.Markdown.Myst.Comments; using Elastic.Markdown.Myst.Directives; +using Elastic.Markdown.Myst.FrontMatter; using Elastic.Markdown.Myst.InlineParsers; using Elastic.Markdown.Myst.Substitution; using Markdig; diff --git a/src/Elastic.Markdown/Myst/ParserContext.cs b/src/Elastic.Markdown/Myst/ParserContext.cs index d4a0067e0..9f07cabfb 100644 --- a/src/Elastic.Markdown/Myst/ParserContext.cs +++ b/src/Elastic.Markdown/Myst/ParserContext.cs @@ -4,6 +4,7 @@ using System.IO.Abstractions; using Elastic.Markdown.IO; +using Elastic.Markdown.Myst.FrontMatter; using Markdig; using Markdig.Parsers; diff --git a/src/Elastic.Markdown/Slices/Directives/Applies.cshtml b/src/Elastic.Markdown/Slices/Directives/Applies.cshtml new file mode 100644 index 000000000..f34525eb8 --- /dev/null +++ b/src/Elastic.Markdown/Slices/Directives/Applies.cshtml @@ -0,0 +1,96 @@ +@using Elastic.Markdown.Myst.FrontMatter +@inherits RazorSlice +

+ + Applies To: + + @if (Model.SelfManaged is not null) + { + if (Model.SelfManaged.Stack is not null) + { + @RenderProduct("Elastic Stack", Model.SelfManaged.Stack) + } + if (Model.SelfManaged.Ece is not null) + { + @RenderProduct("Elastic Cloud Enterprise", Model.SelfManaged.Ece) + } + if (Model.SelfManaged.Eck is not null) + { + @RenderProduct("Elastic Cloud Kubernetes", Model.SelfManaged.Eck) + } + } + @if (Model.Cloud is not null) + { + if (Model.Cloud.Hosted is not null) + { + @RenderProduct("Elastic Cloud Hosted", Model.Cloud.Hosted) + } + if (Model.Cloud.Serverless is not null) + { + @RenderProduct("Serverless", Model.Cloud.Serverless) + } + } +

+ +@functions { + + private string GetLifeCycleClass(ProductLifecycle cycle) + { + switch (cycle) + { + case ProductLifecycle.Deprecated: + case ProductLifecycle.Coming: + case ProductLifecycle.Discontinued: + case ProductLifecycle.Unavailable: + return "muted"; + case ProductLifecycle.GenerallyAvailable: + case ProductLifecycle.TechnicalPreview: + case ProductLifecycle.Beta: + case ProductLifecycle.Development: + return "primary"; + default: + throw new ArgumentOutOfRangeException(nameof(cycle), cycle, null); + } + } + private string GetLifeCycleName(ProductLifecycle cycle) + { + switch (cycle) + { + case ProductLifecycle.TechnicalPreview: + return "Technical Preview"; + case ProductLifecycle.Beta: + return "Beta"; + case ProductLifecycle.Development: + return "Development"; + case ProductLifecycle.Deprecated: + return "Deprecated"; + case ProductLifecycle.Coming: + return "Coming"; + case ProductLifecycle.Discontinued: + return "Discontinued"; + case ProductLifecycle.Unavailable: + return "Unavailable"; + case ProductLifecycle.GenerallyAvailable: + return "GA"; + default: + throw new ArgumentOutOfRangeException(nameof(cycle), cycle, null); + } + } + + private IHtmlContent RenderProduct(string name, ProductAvailability product) + { + var c = GetLifeCycleClass(product.Lifecycle); + + @name + @if (product.Lifecycle != ProductLifecycle.GenerallyAvailable) + { + @GetLifeCycleName(product.Lifecycle) + } + @if (product.Version is not null and not AllVersions) + { + (@product.Version) + } + + return HtmlString.Empty; + } +} \ No newline at end of file diff --git a/src/Elastic.Markdown/Slices/HtmlWriter.cs b/src/Elastic.Markdown/Slices/HtmlWriter.cs index 9706dd4a5..812fbe0d8 100644 --- a/src/Elastic.Markdown/Slices/HtmlWriter.cs +++ b/src/Elastic.Markdown/Slices/HtmlWriter.cs @@ -52,7 +52,8 @@ public async Task RenderLayout(MarkdownFile markdown, Cancel ctx = defau Tree = DocumentationSet.Tree, CurrentDocument = markdown, NavigationHtml = navigationHtml, - UrlPathPrefix = markdown.UrlPathPrefix + UrlPathPrefix = markdown.UrlPathPrefix, + Applies = markdown.YamlFrontMatter?.AppliesTo }); return await slice.RenderAsync(cancellationToken: ctx); } diff --git a/src/Elastic.Markdown/Slices/Index.cshtml b/src/Elastic.Markdown/Slices/Index.cshtml index f136c1d5c..c818ba8ed 100644 --- a/src/Elastic.Markdown/Slices/Index.cshtml +++ b/src/Elastic.Markdown/Slices/Index.cshtml @@ -13,5 +13,9 @@ }

@(Model.Title)

+ @if (Model.Applies is not null) + { + await RenderPartialAsync(Applies.Create(Model.Applies)); + } @(new HtmlString(Model.MarkdownHtml))
diff --git a/src/Elastic.Markdown/Slices/_ViewModels.cs b/src/Elastic.Markdown/Slices/_ViewModels.cs index 59fb58bb3..684396eac 100644 --- a/src/Elastic.Markdown/Slices/_ViewModels.cs +++ b/src/Elastic.Markdown/Slices/_ViewModels.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information using Elastic.Markdown.IO; +using Elastic.Markdown.Myst.FrontMatter; namespace Elastic.Markdown.Slices; @@ -14,6 +15,7 @@ public class IndexViewModel public required MarkdownFile CurrentDocument { get; init; } public required string NavigationHtml { get; init; } public required string? UrlPathPrefix { get; init; } + public required Deployment? Applies { get; init; } } public class LayoutViewModel diff --git a/src/Elastic.Markdown/_static/custom.css b/src/Elastic.Markdown/_static/custom.css index 34884a893..2f2935485 100644 --- a/src/Elastic.Markdown/_static/custom.css +++ b/src/Elastic.Markdown/_static/custom.css @@ -30,4 +30,37 @@ body > footer > div > div > div.sy-foot-copyright > p:nth-child(2){ .yue img { margin-top: 0; margin-bottom: 0; +} + +.applies-badge { + font-size: 1em; + margin-top: 0.4em; +} + +h1 { + padding-bottom: 0.4em; + border-bottom: 1px solid #dfdfdf; +} +.sd-outline-muted +{ + border: 1px solid #dfdfdf; +} +.product-availability { + padding-bottom: 0.8em; + border-bottom: 1px solid #dfdfdf; +} + +h1:has(+ .product-availability) { + margin-bottom: 0.4em; + padding-bottom: 0; + border-bottom: none; +} + +.applies-to-label { + font-size: 1em; + margin-top: 0.4em; + margin-left: 0; + font-weight: bold; + text-align: left; + padding-left: 0; } \ No newline at end of file diff --git a/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs b/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs new file mode 100644 index 000000000..63bbc2a3c --- /dev/null +++ b/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs @@ -0,0 +1,128 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Markdown.Myst.FrontMatter; +using Elastic.Markdown.Tests.Directives; +using FluentAssertions; +using Xunit.Abstractions; +using static Elastic.Markdown.Myst.FrontMatter.ProductLifecycle; + +namespace Elastic.Markdown.Tests.FrontMatter; + +public class ProductConstraintTests(ITestOutputHelper output) : DirectiveTest(output, +""" +--- +title: Elastic Docs v3 +navigation_title: "Documentation Guide" +applies: + stack: ga 8.1 + serverless: tech-preview + hosted: beta 8.1.1 + eck: beta 3.0.2 + ece: unavailable +--- +""" +) +{ + [Fact] + public void Assert() + { + File.YamlFrontMatter.Should().NotBeNull(); + var appliesTo = File.YamlFrontMatter!.AppliesTo; + appliesTo.Should().NotBeNull(); + appliesTo!.Cloud.Should().NotBeNull(); + appliesTo.Cloud!.Serverless.Should().BeEquivalentTo(new ProductAvailability { Lifecycle = TechnicalPreview }); + appliesTo.Cloud!.Hosted.Should().BeEquivalentTo(new ProductAvailability { Lifecycle = Beta, Version = new (8,1,1)}); + appliesTo.SelfManaged.Should().NotBeNull(); + appliesTo.SelfManaged!.Eck.Should().BeEquivalentTo(new ProductAvailability { Lifecycle = Beta, Version = new (3,0,2)}); + appliesTo.SelfManaged!.Ece.Should().BeEquivalentTo(new ProductAvailability { Lifecycle = Unavailable }); + appliesTo.SelfManaged!.Stack.Should().BeEquivalentTo(new ProductAvailability { Lifecycle = GenerallyAvailable, Version = new (8,1,0)}); + } +} + +public abstract class ParsingTests(ITestOutputHelper output, string moniker, ProductAvailability? expected) + : DirectiveTest(output, +$""" +--- +title: Elastic Docs v3 +navigation_title: "Documentation Guide" +applies: + serverless: {moniker} +--- +""" +) +{ + [Fact] + public void Assert() + { + File.YamlFrontMatter.Should().NotBeNull(); + var appliesTo = File.YamlFrontMatter!.AppliesTo; + appliesTo.Should().NotBeNull(); + appliesTo!.Cloud.Should().NotBeNull(); + appliesTo.Cloud!.Serverless.Should().BeEquivalentTo(expected); + } +} + +public class ParsesGa(ITestOutputHelper output) : ParsingTests(output, "ga", new () { Lifecycle = GenerallyAvailable }) ; +public class ParsesDev(ITestOutputHelper output) : ParsingTests(output, "dev", new () { Lifecycle = Development }) ; +public class ParsesDevelopment(ITestOutputHelper output) : ParsingTests(output, "development", new () { Lifecycle = Development }) ; +public class ParsesBeta(ITestOutputHelper output) : ParsingTests(output, "beta", new () { Lifecycle = Beta }) ; +public class ParsesComing(ITestOutputHelper output) : ParsingTests(output, "coming", new () { Lifecycle = Coming }) ; +public class ParsesDeprecated(ITestOutputHelper output) : ParsingTests(output, "deprecated", new () { Lifecycle = Deprecated }) ; +public class ParsesDiscontinued(ITestOutputHelper output) : ParsingTests(output, "discontinued", new () { Lifecycle = Discontinued }) ; +public class ParsesUnavailable(ITestOutputHelper output) : ParsingTests(output, "unavailable", new () { Lifecycle = Unavailable }) ; +public class ParsesTechnicalPreview(ITestOutputHelper output) : ParsingTests(output, "tech-preview", new () { Lifecycle = TechnicalPreview }) ; +public class ParsesPreview(ITestOutputHelper output) : ParsingTests(output, "preview", new () { Lifecycle = TechnicalPreview }) ; +public class ParsesEmpty(ITestOutputHelper output) : ParsingTests(output, "", ProductAvailability.GenerallyAvailable) ; +public class ParsesAll(ITestOutputHelper output) : ParsingTests(output, "all", ProductAvailability.GenerallyAvailable) ; +public class ParsesWithVersion(ITestOutputHelper output) : ParsingTests(output, "ga 7.7.0", new () { Lifecycle = GenerallyAvailable, Version = new (7,7,0) }); +public class ParsesWithAllVersion(ITestOutputHelper output) : ParsingTests(output, "ga all", new () { Lifecycle = GenerallyAvailable, Version = AllVersions.Instance }); + +public class CanSpecifyAllForProductVersions(ITestOutputHelper output) : DirectiveTest(output, +""" +--- +title: Elastic Docs v3 +navigation_title: "Documentation Guide" +applies: + stack: all +--- +""" +) +{ + [Fact] + public void Assert() => + File.YamlFrontMatter!.AppliesTo!.SelfManaged!.Stack.Should().BeEquivalentTo(ProductAvailability.GenerallyAvailable); +} + +public class EmptyProductVersionMeansAll(ITestOutputHelper output) : DirectiveTest(output, +""" +--- +title: Elastic Docs v3 +navigation_title: "Documentation Guide" +applies: + stack: +--- +""" +) +{ + [Fact] + public void Assert() => + File.YamlFrontMatter!.AppliesTo!.SelfManaged!.Stack.Should().BeEquivalentTo(ProductAvailability.GenerallyAvailable); +} + +public class EmptyCloudSetsAllProductsToAll(ITestOutputHelper output) : DirectiveTest(output, +""" +--- +title: Elastic Docs v3 +navigation_title: "Documentation Guide" +applies: + hosted: +--- +""" +) +{ + [Fact] + public void Assert() => + File.YamlFrontMatter!.AppliesTo!.Cloud!.Hosted.Should().BeEquivalentTo(ProductAvailability.GenerallyAvailable); +} diff --git a/tests/Elastic.Markdown.Tests/Directives/YamlFrontMatterTests.cs b/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs similarity index 93% rename from tests/Elastic.Markdown.Tests/Directives/YamlFrontMatterTests.cs rename to tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs index c478f37c3..178773ddc 100644 --- a/tests/Elastic.Markdown.Tests/Directives/YamlFrontMatterTests.cs +++ b/tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs @@ -1,10 +1,12 @@ // Licensed to Elasticsearch B.V under one or more agreements. // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information + +using Elastic.Markdown.Tests.Directives; using FluentAssertions; using Xunit.Abstractions; -namespace Elastic.Markdown.Tests.Directives; +namespace Elastic.Markdown.Tests.FrontMatter; public class YamlFrontMatterTests(ITestOutputHelper output) : DirectiveTest(output, """