Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
101 lines
3.0 KiB
C#
101 lines
3.0 KiB
C#
using System.Text.Json.Nodes;
|
|
using System.Text.RegularExpressions;
|
|
using StellaOps.Notify.Models;
|
|
|
|
namespace StellaOps.Notifier.Worker.Processing;
|
|
|
|
/// <summary>
|
|
/// Simple Handlebars-like template renderer supporting {{property}} and {{#each}} blocks.
|
|
/// </summary>
|
|
public sealed partial class SimpleTemplateRenderer : INotifyTemplateRenderer
|
|
{
|
|
private static readonly Regex PlaceholderPattern = PlaceholderRegex();
|
|
private static readonly Regex EachBlockPattern = EachBlockRegex();
|
|
|
|
public string Render(NotifyTemplate template, JsonNode? payload)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(template);
|
|
|
|
var body = template.Body;
|
|
if (string.IsNullOrWhiteSpace(body))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
// Process {{#each}} blocks first
|
|
body = ProcessEachBlocks(body, payload);
|
|
|
|
// Then substitute simple placeholders
|
|
body = SubstitutePlaceholders(body, payload);
|
|
|
|
return body;
|
|
}
|
|
|
|
private static string ProcessEachBlocks(string body, JsonNode? payload)
|
|
{
|
|
return EachBlockPattern.Replace(body, match =>
|
|
{
|
|
var collectionPath = match.Groups[1].Value.Trim();
|
|
var innerTemplate = match.Groups[2].Value;
|
|
|
|
var collection = ResolvePath(payload, collectionPath);
|
|
if (collection is not JsonObject obj)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var results = new List<string>();
|
|
foreach (var (key, value) in obj)
|
|
{
|
|
var itemResult = innerTemplate
|
|
.Replace("{{@key}}", key)
|
|
.Replace("{{this}}", value?.ToString() ?? string.Empty);
|
|
results.Add(itemResult);
|
|
}
|
|
|
|
return string.Join(string.Empty, results);
|
|
});
|
|
}
|
|
|
|
private static string SubstitutePlaceholders(string body, JsonNode? payload)
|
|
{
|
|
return PlaceholderPattern.Replace(body, match =>
|
|
{
|
|
var path = match.Groups[1].Value.Trim();
|
|
var resolved = ResolvePath(payload, path);
|
|
return resolved?.ToString() ?? string.Empty;
|
|
});
|
|
}
|
|
|
|
private static JsonNode? ResolvePath(JsonNode? root, string path)
|
|
{
|
|
if (root is null || string.IsNullOrWhiteSpace(path))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var segments = path.Split('.');
|
|
var current = root;
|
|
|
|
foreach (var segment in segments)
|
|
{
|
|
if (current is JsonObject obj && obj.TryGetPropertyValue(segment, out var next))
|
|
{
|
|
current = next;
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
[GeneratedRegex(@"\{\{([^#/}]+)\}\}", RegexOptions.Compiled)]
|
|
private static partial Regex PlaceholderRegex();
|
|
|
|
[GeneratedRegex(@"\{\{#each\s+([^}]+)\}\}(.*?)\{\{/each\}\}", RegexOptions.Compiled | RegexOptions.Singleline)]
|
|
private static partial Regex EachBlockRegex();
|
|
}
|