Skip to content

Commit

Permalink
Add initial support for page level annotations for product support (#103
Browse files Browse the repository at this point in the history
)
  • Loading branch information
Mpdreamz authored Dec 13, 2024
1 parent 4bf4291 commit 20d5f86
Show file tree
Hide file tree
Showing 18 changed files with 605 additions and 5 deletions.
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

0 comments on commit 20d5f86

Please sign in to comment.