From 6c5b38756f68faad1d12c20d30811b54bd276621 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 13 Dec 2024 12:39:48 +0100 Subject: [PATCH] update applies to syntax to include product lifecycle --- src/Elastic.Markdown/IO/MarkdownFile.cs | 1 + .../Myst/Directives/IncludeBlock.cs | 1 + .../Myst/FrontMatter/FrontMatterParser.cs | 46 +++++ .../Myst/FrontMatterParser.cs | 195 ------------------ src/Elastic.Markdown/Myst/MarkdownParser.cs | 1 + src/Elastic.Markdown/Myst/ParserContext.cs | 1 + .../FrontMatter/ProductConstraintTests.cs | 79 +++++-- 7 files changed, 107 insertions(+), 217 deletions(-) create mode 100644 src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs delete mode 100644 src/Elastic.Markdown/Myst/FrontMatterParser.cs diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index ece680f..477b619 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 974fb32..28dd4c5 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/FrontMatterParser.cs b/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs new file mode 100644 index 0000000..694045a --- /dev/null +++ b/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs @@ -0,0 +1,46 @@ +// 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; + +[YamlStaticContext] +public partial class YamlFrontMatterStaticContext; + +[YamlSerializable] +public class YamlFrontMatter +{ + [YamlMember(Alias = "title")] + public string? Title { get; set; } + + [YamlMember(Alias = "navigation_title")] + public string? NavigationTitle { get; set; } + + [YamlMember(Alias = "sub")] + public Dictionary? Properties { get; set; } + + + [YamlMember(Alias = "applies")] + public Deployment? AppliesTo { get; set; } +} + +public static class FrontMatterParser +{ + public static YamlFrontMatter Deserialize(string yaml) + { + var input = new StringReader(yaml); + + var deserializer = new StaticDeserializerBuilder(new YamlFrontMatterStaticContext()) + .IgnoreUnmatchedProperties() + .WithTypeConverter(new SemVersionConverter()) + .WithTypeConverter(new DeploymentConverter()) + .Build(); + + var frontMatter = deserializer.Deserialize(input); + return frontMatter; + + } +} + diff --git a/src/Elastic.Markdown/Myst/FrontMatterParser.cs b/src/Elastic.Markdown/Myst/FrontMatterParser.cs deleted file mode 100644 index 3aa5fb1..0000000 --- a/src/Elastic.Markdown/Myst/FrontMatterParser.cs +++ /dev/null @@ -1,195 +0,0 @@ -// 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; -using YamlDotNet.Serialization.NamingConventions; - -namespace Elastic.Markdown.Myst; - -[YamlStaticContext] -public partial class YamlFrontMatterStaticContext; - -[YamlSerializable] -public class YamlFrontMatter -{ - [YamlMember(Alias = "title")] - public string? Title { get; set; } - - [YamlMember(Alias = "navigation_title")] - public string? NavigationTitle { get; set; } - - [YamlMember(Alias = "sub")] - public Dictionary? Properties { get; set; } - - - [YamlMember(Alias = "applies")] - public Deployment? AppliesTo { get; set; } -} - -[YamlSerializable] -public class Deployment -{ - [YamlMember(Alias = "self")] - public SelfManagedDeployment? SelfManaged { get; set; } - - [YamlMember(Alias = "cloud")] - public CloudManagedDeployment? Cloud { get; set; } -} - -public class AllVersions() : SemVersion(9999, 9999, 9999) -{ - public static AllVersions Instance { get; } = new (); -} - -[YamlSerializable] -public class SelfManagedDeployment -{ - [YamlMember(Alias = "stack")] - public SemVersion? Stack { get; set; } - - [YamlMember(Alias = "ece")] - public SemVersion? Ece { get; set; } - - [YamlMember(Alias = "eck")] - public SemVersion? Eck { get; set; } - - public static SelfManagedDeployment All { get; } = new() - { - Stack = AllVersions.Instance, - Ece = AllVersions.Instance, - Eck = AllVersions.Instance - }; -} - -[YamlSerializable] -public class CloudManagedDeployment -{ - [YamlMember(Alias = "hosted")] - public SemVersion? Hosted { get; set; } - - [YamlMember(Alias = "serverless")] - public SemVersion? Serverless { get; set; } - - public static CloudManagedDeployment All { get; } = new() - { - Hosted = AllVersions.Instance, - Serverless = AllVersions.Instance - }; - -} - -public static class FrontMatterParser -{ - public static YamlFrontMatter Deserialize(string yaml) - { - var input = new StringReader(yaml); - - var deserializer = new StaticDeserializerBuilder(new YamlFrontMatterStaticContext()) - .IgnoreUnmatchedProperties() - .WithTypeConverter(new SemVersionConverter()) - .WithTypeConverter(new CloudManagedSerializer()) - .WithTypeConverter(new SelfManagedSerializer()) - .Build(); - - var frontMatter = deserializer.Deserialize(input); - return frontMatter; - - } -} - -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, "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 SemVersion? Parse(string? value) => - value?.Trim().ToLowerInvariant() switch - { - null => AllVersions.Instance, - "all" => AllVersions.Instance, - "" => AllVersions.Instance, - _ => SemVersion.TryParse(value, out var v) ? v : null - }; -} - -public class CloudManagedSerializer : IYamlTypeConverter -{ - public bool Accepts(Type type) => type == typeof(CloudManagedDeployment); - - public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) - { - if (parser.TryConsume(out var value)) - { - if (string.IsNullOrWhiteSpace(value.Value)) - return CloudManagedDeployment.All; - if (string.Equals(value.Value, "all", StringComparison.InvariantCultureIgnoreCase)) - return CloudManagedDeployment.All; - } - var x = rootDeserializer.Invoke(typeof(Dictionary)); - if (x is not Dictionary { Count: > 0 } dictionary) - return null; - - var cloudManaged = new CloudManagedDeployment(); - if (dictionary.TryGetValue("hosted", out var v)) - cloudManaged.Hosted = SemVersionConverter.Parse(v); - if (dictionary.TryGetValue("serverless", out v)) - cloudManaged.Serverless = SemVersionConverter.Parse(v); - return cloudManaged; - - } - - public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => - serializer.Invoke(value, type); -} - -public class SelfManagedSerializer : IYamlTypeConverter -{ - public bool Accepts(Type type) => type == typeof(SelfManagedDeployment); - - public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) - { - if (parser.TryConsume(out var value)) - { - if (string.IsNullOrWhiteSpace(value.Value)) - return SelfManagedDeployment.All; - if (string.Equals(value.Value, "all", StringComparison.InvariantCultureIgnoreCase)) - return SelfManagedDeployment.All; - } - var x = rootDeserializer.Invoke(typeof(Dictionary)); - if (x is not Dictionary { Count: > 0 } dictionary) - return null; - - var deployment = new SelfManagedDeployment(); - if (dictionary.TryGetValue("stack", out var v)) - deployment.Stack = SemVersionConverter.Parse(v); - if (dictionary.TryGetValue("ece", out v)) - deployment.Ece = SemVersionConverter.Parse(v); - if (dictionary.TryGetValue("eck", out v)) - deployment.Eck = SemVersionConverter.Parse(v); - return deployment; - - } - - public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => - serializer.Invoke(value, type); -} diff --git a/src/Elastic.Markdown/Myst/MarkdownParser.cs b/src/Elastic.Markdown/Myst/MarkdownParser.cs index 925af0d..9fccbc7 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 d4a0067..9f07cab 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/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs b/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs index 9ece827..63bbc2a 100644 --- a/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs +++ b/tests/Elastic.Markdown.Tests/FrontMatter/ProductConstraintTests.cs @@ -2,11 +2,11 @@ // 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 Elastic.Markdown.Myst; +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; @@ -16,47 +16,83 @@ public class ProductConstraintTests(ITestOutputHelper output) : DirectiveTest(ou title: Elastic Docs v3 navigation_title: "Documentation Guide" applies: - self: - stack: 7.7 - cloud: - serverless: 1.0.0 + stack: ga 8.1 + serverless: tech-preview + hosted: beta 8.1.1 + eck: beta 3.0.2 + ece: unavailable --- """ ) { [Fact] - public void ReadsTitle() => File.Title.Should().Be("Elastic Docs v3"); - - [Fact] - public void ReadsNavigationTitle() => File.NavigationTitle.Should().Be("Documentation Guide"); + 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 ReadsSubstitutions() + public void Assert() { File.YamlFrontMatter.Should().NotBeNull(); var appliesTo = File.YamlFrontMatter!.AppliesTo; appliesTo.Should().NotBeNull(); - appliesTo!.SelfManaged.Should().NotBeNull(); - appliesTo.Cloud.Should().NotBeNull(); - appliesTo.Cloud!.Serverless.Should().BeEquivalentTo(new SemVersion(1,0,0)); + 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: - self: - stack: all + stack: all --- """ ) { [Fact] public void Assert() => - File.YamlFrontMatter!.AppliesTo!.SelfManaged!.Stack.Should().BeEquivalentTo(AllVersions.Instance); + File.YamlFrontMatter!.AppliesTo!.SelfManaged!.Stack.Should().BeEquivalentTo(ProductAvailability.GenerallyAvailable); } public class EmptyProductVersionMeansAll(ITestOutputHelper output) : DirectiveTest(output, @@ -65,15 +101,14 @@ public class EmptyProductVersionMeansAll(ITestOutputHelper output) : DirectiveTe title: Elastic Docs v3 navigation_title: "Documentation Guide" applies: - self: - stack: + stack: --- """ ) { [Fact] public void Assert() => - File.YamlFrontMatter!.AppliesTo!.SelfManaged!.Stack.Should().BeEquivalentTo(AllVersions.Instance); + File.YamlFrontMatter!.AppliesTo!.SelfManaged!.Stack.Should().BeEquivalentTo(ProductAvailability.GenerallyAvailable); } public class EmptyCloudSetsAllProductsToAll(ITestOutputHelper output) : DirectiveTest(output, @@ -82,12 +117,12 @@ public class EmptyCloudSetsAllProductsToAll(ITestOutputHelper output) : Directiv title: Elastic Docs v3 navigation_title: "Documentation Guide" applies: - cloud: + hosted: --- """ ) { [Fact] public void Assert() => - File.YamlFrontMatter!.AppliesTo!.Cloud!.Hosted.Should().BeEquivalentTo(AllVersions.Instance); + File.YamlFrontMatter!.AppliesTo!.Cloud!.Hosted.Should().BeEquivalentTo(ProductAvailability.GenerallyAvailable); }