Skip to content

Add initial support for page level annotations for product support #103

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 13, 2024
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
48 changes: 48 additions & 0 deletions docs/source/markup/applies.md
Original file line number Diff line number Diff line change
@@ -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

```
<product>: <lifecycle> [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.
10 changes: 9 additions & 1 deletion src/Elastic.Markdown/Helpers/SemVersion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Elastic.Markdown.Helpers;
/// <summary>
/// A semver2 compatible version.
/// </summary>
public sealed class SemVersion :
public class SemVersion :
IEquatable<SemVersion>,
IComparable<SemVersion>,
IComparable
Expand Down Expand Up @@ -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();

/// <summary>
///
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/Elastic.Markdown/IO/MarkdownFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/Elastic.Markdown/Myst/Directives/IncludeBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
50 changes: 50 additions & 0 deletions src/Elastic.Markdown/Myst/FrontMatter/AllVersions.cs
Original file line number Diff line number Diff line change
@@ -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<Scalar>();
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;
}
}

120 changes: 120 additions & 0 deletions src/Elastic.Markdown/Myst/FrontMatter/Deployment.cs
Original file line number Diff line number Diff line change
@@ -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<Scalar>(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<string, string>));
if (x is not Dictionary<string, string> { 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);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -20,6 +20,10 @@ public class YamlFrontMatter

[YamlMember(Alias = "sub")]
public Dictionary<string, string>? Properties { get; set; }


[YamlMember(Alias = "applies")]
public Deployment? AppliesTo { get; set; }
}

public static class FrontMatterParser
Expand All @@ -30,10 +34,13 @@ public static YamlFrontMatter Deserialize(string yaml)

var deserializer = new StaticDeserializerBuilder(new YamlFrontMatterStaticContext())
.IgnoreUnmatchedProperties()
.WithTypeConverter(new SemVersionConverter())
.WithTypeConverter(new DeploymentConverter())
.Build();

var frontMatter = deserializer.Deserialize<YamlFrontMatter>(input);
return frontMatter;

}
}

61 changes: 61 additions & 0 deletions src/Elastic.Markdown/Myst/FrontMatter/ProductAvailability.cs
Original file line number Diff line number Diff line change
@@ -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
};

// <lifecycle> [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;
}
}
36 changes: 36 additions & 0 deletions src/Elastic.Markdown/Myst/FrontMatter/ProductLifecycle.cs
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading