diff --git a/Directory.Build.props b/Directory.Build.props
index 544d9346..bee62dff 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -4,7 +4,6 @@
latest
enable
enable
- git
WeihanLi
Copyright 2017-$([System.DateTime]::Now.Year) (c) WeihanLi
$(NoWarn);NU5048;CS1591;NETSDK1057
diff --git a/Directory.Packages.props b/Directory.Packages.props
index f01ffd0b..df472839 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,7 +5,7 @@
6.0.0
7.0.0
8.0.0
- 9.0.0-preview.7.24405.7
+ 9.0.0-rc.1.24431.7
@@ -17,13 +17,13 @@
-
+
-
+
-
-
+
+
diff --git a/README.md b/README.md
index 0b15fc71..c96b1c36 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,7 @@
- Logging Framework(结合Serilog/微软日志框架实现的日志框架)
- Dapper-like Ado.Net extensions(类似 Dapper 的 Ado.Net 扩展)
- TOTP implement(TOTP算法实现)
+- Template Engine(自定义模板引擎)
- and more ...
## Release Notes
diff --git a/build/version.props b/build/version.props
index 249bcd7c..e2759345 100644
--- a/build/version.props
+++ b/build/version.props
@@ -2,7 +2,7 @@
1
0
- 69
+ 70
$(VersionMajor).$(VersionMinor).$(VersionPatch)
diff --git a/samples/DotNetCoreSample/Program.cs b/samples/DotNetCoreSample/Program.cs
index 3d07a9a2..9bcfc384 100644
--- a/samples/DotNetCoreSample/Program.cs
+++ b/samples/DotNetCoreSample/Program.cs
@@ -343,7 +343,9 @@
// await InvokeHelper.TryInvokeAsync(EventTest.MainTest);
-InvokeHelper.TryInvoke(CommandExecutorTest.MainTest);
+// InvokeHelper.TryInvoke(CommandExecutorTest.MainTest);
+
+await InvokeHelper.TryInvokeAsync(TemplatingSample.MainTest);
ConsoleHelper.ReadKeyWithPrompt("Press any key to exit");
diff --git a/samples/DotNetCoreSample/TemplatingSample.cs b/samples/DotNetCoreSample/TemplatingSample.cs
index b8bad829..a4382229 100644
--- a/samples/DotNetCoreSample/TemplatingSample.cs
+++ b/samples/DotNetCoreSample/TemplatingSample.cs
@@ -15,6 +15,8 @@ public static async Task MainTest()
var engine = TemplateEngine.CreateDefault();
var result = await engine.RenderAsync("Hello {{Name}}", new { Name = ".NET" });
Console.WriteLine(result);
+ Console.WriteLine(await engine.RenderAsync("Hello {{Name | toTitle }}", new { Name = "mike" }));
+ Console.WriteLine(await engine.RenderAsync("Today is {{ date | format:yyyy-MM-dd }}", new { date = DateTime.Today }));
}
{
diff --git a/src/WeihanLi.Common/Extensions/CoreExtension.cs b/src/WeihanLi.Common/Extensions/CoreExtension.cs
index 13c68193..e2c6e279 100644
--- a/src/WeihanLi.Common/Extensions/CoreExtension.cs
+++ b/src/WeihanLi.Common/Extensions/CoreExtension.cs
@@ -1,8 +1,9 @@
-using System.Collections.Concurrent;
+// Copyright (c) Weihan Li. All rights reserved.
+// Licensed under the Apache license.
+
+using System.Collections.Concurrent;
using System.ComponentModel;
-using System.Diagnostics.CodeAnalysis;
using System.Globalization;
-using System.Net;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
@@ -1638,8 +1639,8 @@ public static bool ToBoolean(this string? value, bool defaultValue = false)
return value switch
{
null => defaultValue,
- "" or "1" => true,
- "0" => false,
+ "" or "1" or "y" or "yes" => true,
+ "0" or "n" or "no" => false,
_ => bool.TryParse(value, out var val) ? val : defaultValue
};
}
diff --git a/src/WeihanLi.Common/Helpers/ApplicationHelper.cs b/src/WeihanLi.Common/Helpers/ApplicationHelper.cs
index 7e2d6230..5a39fec5 100644
--- a/src/WeihanLi.Common/Helpers/ApplicationHelper.cs
+++ b/src/WeihanLi.Common/Helpers/ApplicationHelper.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache license.
using System.Reflection;
+using System.Runtime;
using System.Runtime.InteropServices;
using WeihanLi.Extensions;
@@ -42,6 +43,7 @@ public static LibraryInfo GetLibraryInfo(Assembly assembly)
var informationalVersionSplit = assemblyInformation.InformationalVersion.Split('+');
return new LibraryInfo()
{
+ VersionWithHash = assemblyInformation.InformationalVersion,
LibraryVersion = informationalVersionSplit[0],
LibraryHash = informationalVersionSplit.Length > 1 ? informationalVersionSplit[1] : string.Empty,
RepositoryUrl = repositoryUrl
@@ -55,8 +57,8 @@ public static LibraryInfo GetLibraryInfo(Assembly assembly)
};
}
- private static readonly Lazy _runtimeInfoLazy = new(GetRuntimeInfo);
- public static RuntimeInfo RuntimeInfo => _runtimeInfoLazy.Value;
+ private static readonly Lazy LazyRuntimeInfo = new(GetRuntimeInfo);
+ public static RuntimeInfo RuntimeInfo => LazyRuntimeInfo.Value;
///
/// Get dotnet executable path
@@ -140,9 +142,6 @@ private static RuntimeInfo GetRuntimeInfo()
var runtimeInfo = new RuntimeInfo()
{
Version = Environment.Version.ToString(),
- ProcessorCount = Environment.ProcessorCount,
- FrameworkDescription = RuntimeInformation.FrameworkDescription,
- WorkingDirectory = Environment.CurrentDirectory,
#if NET6_0_OR_GREATER
ProcessId = Environment.ProcessId,
@@ -152,18 +151,25 @@ private static RuntimeInfo GetRuntimeInfo()
ProcessId = currentProcess.Id,
ProcessPath = currentProcess.MainModule?.FileName ?? string.Empty,
#endif
+
+ ProcessorCount = Environment.ProcessorCount,
+ FrameworkDescription = RuntimeInformation.FrameworkDescription,
+ WorkingDirectory = Environment.CurrentDirectory,
OSArchitecture = RuntimeInformation.OSArchitecture.ToString(),
OSDescription = RuntimeInformation.OSDescription,
OSVersion = Environment.OSVersion.ToString(),
MachineName = Environment.MachineName,
UserName = Environment.UserName,
+ IsServerGC = GCSettings.IsServerGC,
+
IsInContainer = IsInContainer(),
IsInKubernetes = IsInKubernetesCluster(),
KubernetesNamespace = GetKubernetesNamespace(),
LibraryVersion = libInfo.LibraryVersion,
LibraryHash = libInfo.LibraryHash,
+ VersionWithHash = libInfo.VersionWithHash,
RepositoryUrl = libInfo.RepositoryUrl,
};
return runtimeInfo;
@@ -230,9 +236,11 @@ private static bool IsInKubernetesCluster()
public class LibraryInfo
{
+ private string? _versionWithHash;
public required string LibraryVersion { get; init; }
public required string LibraryHash { get; init; }
public required string RepositoryUrl { get; init; }
+ public string VersionWithHash { get => _versionWithHash ?? LibraryVersion; init => _versionWithHash = value; }
}
public class RuntimeInfo : LibraryInfo
@@ -250,6 +258,12 @@ public class RuntimeInfo : LibraryInfo
public required string RuntimeIdentifier { get; init; }
#endif
+ // GC
+ /// Gets a value that indicates whether server garbage collection is enabled.
+ ///
+ /// if server garbage collection is enabled; otherwise, .
+ public required bool IsServerGC { get; init; }
+
public required string WorkingDirectory { get; init; }
public required int ProcessId { get; init; }
public required string ProcessPath { get; init; }
diff --git a/src/WeihanLi.Common/Helpers/EnvHelper.cs b/src/WeihanLi.Common/Helpers/EnvHelper.cs
index f4c05f6e..54735061 100644
--- a/src/WeihanLi.Common/Helpers/EnvHelper.cs
+++ b/src/WeihanLi.Common/Helpers/EnvHelper.cs
@@ -1,7 +1,7 @@
// Copyright (c) Weihan Li. All rights reserved.
// Licensed under the Apache license.
-using System.Diagnostics.CodeAnalysis;
+using WeihanLi.Extensions;
namespace WeihanLi.Common.Helpers;
@@ -12,4 +12,10 @@ public static class EnvHelper
{
return Environment.GetEnvironmentVariable(envName) ?? defaultValue;
}
+
+ public static bool BooleanVal(string envName, bool defaultValue = false)
+ {
+ var val = Environment.GetEnvironmentVariable(envName);
+ return val.ToBoolean(defaultValue);
+ }
}
diff --git a/src/WeihanLi.Common/Template/ConfigurationRenderMiddleare.cs b/src/WeihanLi.Common/Template/ConfigurationRenderMiddleare.cs
index 25ac9d0d..8ace6771 100644
--- a/src/WeihanLi.Common/Template/ConfigurationRenderMiddleare.cs
+++ b/src/WeihanLi.Common/Template/ConfigurationRenderMiddleare.cs
@@ -5,16 +5,20 @@
namespace WeihanLi.Common.Template;
-internal sealed class ConfigurationRenderMiddleware(IConfiguration? configuration = null) : IRenderMiddleware
+internal sealed class ConfigurationRenderMiddleware(IConfiguration? configuration = null)
+ : IRenderMiddleware
{
- private const string Prefix = "$config ";
+ private const string Prefix = "$config";
public Task InvokeAsync(TemplateRenderContext context, Func next)
{
- if (configuration != null)
+ if (configuration is not null)
{
- foreach (var variable in context.Variables.Where(x => x.StartsWith(Prefix) && !context.Parameters.ContainsKey(x)))
+ foreach (var pair in context.Inputs
+ .Where(x => x.Key.Prefix is Prefix
+ && x.Value is null)
+ )
{
- context.Parameters[variable] = configuration[variable[Prefix.Length..]];
+ context.Inputs[pair.Key] = configuration[pair.Key.VariableName];
}
}
diff --git a/src/WeihanLi.Common/Template/DefaultRenderMiddleware.cs b/src/WeihanLi.Common/Template/DefaultRenderMiddleware.cs
index 11bd4f98..6f27d306 100644
--- a/src/WeihanLi.Common/Template/DefaultRenderMiddleware.cs
+++ b/src/WeihanLi.Common/Template/DefaultRenderMiddleware.cs
@@ -3,10 +3,26 @@
namespace WeihanLi.Common.Template;
-internal sealed class DefaultRenderMiddleware : IRenderMiddleware
+internal sealed class DefaultRenderMiddleware(Dictionary pipes) : IRenderMiddleware
{
public async Task InvokeAsync(TemplateRenderContext context, Func next)
{
await next(context);
+ foreach (var input in context.Inputs.Keys)
+ {
+ var value = context.Inputs[input];
+ if (input.Pipes is { Length: > 0 })
+ {
+ foreach (var pipeInput in input.Pipes)
+ {
+ if (pipes.TryGetValue(pipeInput.PipeName, out var pipe))
+ {
+ value = pipe.Convert(value, pipeInput.Arguments);
+ }
+ }
+ }
+ // replace input with value
+ context.RenderedText = context.RenderedText.Replace(input.Input, value as string ?? value?.ToString());
+ }
}
}
diff --git a/src/WeihanLi.Common/Template/DefaultTemplateParser.cs b/src/WeihanLi.Common/Template/DefaultTemplateParser.cs
index a9f752ee..0e28601b 100644
--- a/src/WeihanLi.Common/Template/DefaultTemplateParser.cs
+++ b/src/WeihanLi.Common/Template/DefaultTemplateParser.cs
@@ -7,19 +7,79 @@ namespace WeihanLi.Common.Template;
internal sealed class DefaultTemplateParser : ITemplateParser
{
- private const string VariableRegexExp = @"\{\{(?[\w\$\s:]+)\}\}";
- private static readonly Regex VariableRegex = new(VariableRegexExp, RegexOptions.Compiled);
+ private const string VariableGroupRegexExp = @"\{\{(?[\w\$\s:\.]+)(?|[^\{\}]*)\}\}";
+ private static readonly Regex VariableRegex = new(VariableGroupRegexExp, RegexOptions.Compiled);
public Task ParseAsync(string text)
{
- var variables = new HashSet();
+ List inputs = [];
var match = VariableRegex.Match(text);
while (match.Success)
{
- var variable = match.Groups["Variable"].Value;
- variables.Add(variable);
+ var pipes = Array.Empty();
+ var variableInput = match.Groups["Variable"].Value;
+ var variableName = variableInput.Trim();
+ string? prefix = null;
+
+ var prefixIndex = variableName.IndexOf('$'); // prefix start
+ if (prefixIndex >= 0)
+ {
+ var nameIndex = variableName.IndexOf(' ', prefixIndex); // name start
+ prefix = variableName[..nameIndex].Trim();
+ variableName = variableName[nameIndex..].Trim();
+ }
+
+ var pipeValue = match.Groups["Pipe"]?.Value.Trim();
+ if (!string.IsNullOrEmpty(pipeValue))
+ {
+ var pipeIndex = pipeValue.IndexOf('|');
+ if (pipeIndex < 0)
+ {
+ match = match.NextMatch();
+ continue;
+ }
+
+ // exact pipes
+ pipeValue = pipeValue[pipeIndex..].Trim();
+ var pipeInputs = pipeValue!.Split(['|'], StringSplitOptions.RemoveEmptyEntries);
+ pipes = pipeInputs.Select(p =>
+ {
+ var pipeName = p.Trim();
+ var arguments = Array.Empty();
+ var sep = pipeName.IndexOf(':');
+ if (sep >= 0)
+ {
+ if (sep + 1 < pipeName.Length)
+ {
+ var argumentsText = pipeName[(sep + 1)..].Trim();
+ arguments =
+#if NET
+ argumentsText.Split([':'],
+ StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+#else
+ argumentsText.Split([':'], StringSplitOptions.RemoveEmptyEntries)
+ .Select(x=> x.Trim()).ToArray();
+#endif
+ }
+
+ pipeName = pipeName[..sep].Trim();
+ }
+
+ return new TemplatePipeInput() { PipeName = pipeName, Arguments = arguments };
+ }).ToArray();
+ }
+
+ var input = new TemplateInput
+ {
+ Input = match.Value,
+ Prefix = prefix,
+ VariableName = variableName,
+ Pipes = pipes
+ };
+ inputs.Add(input);
+
match = match.NextMatch();
}
- var context = new TemplateRenderContext(text, variables);
+ var context = new TemplateRenderContext(text, inputs);
return Task.FromResult(context);
}
}
diff --git a/src/WeihanLi.Common/Template/DefaultTemplateRenderer.cs b/src/WeihanLi.Common/Template/DefaultTemplateRenderer.cs
index 82a96fdf..955f8aef 100644
--- a/src/WeihanLi.Common/Template/DefaultTemplateRenderer.cs
+++ b/src/WeihanLi.Common/Template/DefaultTemplateRenderer.cs
@@ -5,21 +5,26 @@
namespace WeihanLi.Common.Template;
-internal sealed class DefaultTemplateRenderer(Func renderFunc) : ITemplateRenderer
+internal sealed class DefaultTemplateRenderer(Func renderFunc)
+ : ITemplateRenderer
{
public async Task RenderAsync(TemplateRenderContext context, object? globals)
{
- if (context.Text.IsNullOrWhiteSpace() || context.Variables.IsNullOrEmpty())
+ if (context.Text.IsNullOrWhiteSpace() || context.Inputs.IsNullOrEmpty())
return context.Text;
-
- context.Parameters = globals.ParseParamDictionary();
- await renderFunc.Invoke(context).ConfigureAwait(false);
- foreach (var parameter in context.Parameters)
+
+ var parameters = globals.ParseParamDictionary();
+ if (parameters is { Count: > 0 })
{
- context.RenderedText = context.RenderedText.Replace(
- $"{{{{{parameter.Key}}}}}", parameter.Value?.ToString()
- );
+ foreach (var input in context.Inputs.Keys.Where(x => x.Prefix is null))
+ {
+ if (parameters.TryGetValue(input.VariableName, out var value))
+ {
+ context.Inputs[input] = value;
+ }
+ }
}
+ await renderFunc.Invoke(context).ConfigureAwait(false);
return context.RenderedText;
}
}
diff --git a/src/WeihanLi.Common/Template/DependencyInjectionExtensions.cs b/src/WeihanLi.Common/Template/DependencyInjectionExtensions.cs
index 0b9301d0..85acefce 100644
--- a/src/WeihanLi.Common/Template/DependencyInjectionExtensions.cs
+++ b/src/WeihanLi.Common/Template/DependencyInjectionExtensions.cs
@@ -11,7 +11,10 @@ namespace WeihanLi.Common.Template;
public static class DependencyInjectionExtensions
{
- public static IServiceCollection AddTemplateEngine(this IServiceCollection services, Action? optionsConfigure = null)
+ public static ITemplateEngineServiceBuilder AddTemplateEngine(
+ this IServiceCollection services,
+ Action? optionsConfigure = null
+ )
{
Guard.NotNull(services);
if (services.Any(x => x.ServiceType == typeof(ITemplateEngine)))
@@ -20,20 +23,33 @@ public static IServiceCollection AddTemplateEngine(this IServiceCollection servi
if (optionsConfigure != null)
services.AddOptions().Configure(optionsConfigure);
+ services.TryAddSingleton();
services.TryAddSingleton();
services.TryAddSingleton();
- services.TryAddSingleton();
+
+ var serviceBuilder = new TemplateEngineServiceBuilder(services);
+
+ serviceBuilder.AddPipe()
+ .AddPipe()
+ .AddPipe()
+ .AddPipe()
+ ;
- services.TryAddEnumerable(ServiceDescriptor.Singleton());
- services.TryAddEnumerable(ServiceDescriptor.Singleton());
- services.AddSingleton(sp =>
- {
- var configuration = sp.GetService>()?.Value.Configuration
- ?? sp.GetService();
- return new ConfigurationRenderMiddleware(configuration);
- });
+ serviceBuilder.AddRenderMiddleware(sp =>
+ {
+ var pipes = sp.GetServices().ToDictionary(p => p.Name, p => p);
+ return new DefaultRenderMiddleware(pipes);
+ })
+ .AddRenderMiddleware()
+ .AddRenderMiddleware(sp =>
+ {
+ var configuration = sp.GetService>()?.Value.Configuration
+ ?? sp.GetService();
+ return new ConfigurationRenderMiddleware(configuration);
+ })
+ ;
- services.TryAddSingleton(sp =>
+ services.AddSingleton(sp =>
{
var pipelineBuilder = PipelineBuilder.CreateAsync();
foreach (var middleware in sp.GetServices())
@@ -43,6 +59,27 @@ public static IServiceCollection AddTemplateEngine(this IServiceCollection servi
return pipelineBuilder.Build();
});
- return services;
+ return serviceBuilder;
+ }
+
+ public static ITemplateEngineServiceBuilder AddRenderMiddleware(
+ this ITemplateEngineServiceBuilder serviceBuilder,
+ Func? middlewareFactory = null
+ ) where TMiddleware : class, IRenderMiddleware
+ {
+ var serviceDescriptor = middlewareFactory is null
+ ? ServiceDescriptor.Singleton()
+ : ServiceDescriptor.Singleton(middlewareFactory)
+ ;
+ serviceBuilder.Services.Add(serviceDescriptor);
+ return serviceBuilder;
+ }
+
+ public static ITemplateEngineServiceBuilder AddPipe
+ (this ITemplateEngineServiceBuilder serviceBuilder)
+ where TPipe : class, ITemplatePipe
+ {
+ serviceBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton());
+ return serviceBuilder;
}
}
diff --git a/src/WeihanLi.Common/Template/EnvRenderMiddleware.cs b/src/WeihanLi.Common/Template/EnvRenderMiddleware.cs
index b52e9c7c..5bc11793 100644
--- a/src/WeihanLi.Common/Template/EnvRenderMiddleware.cs
+++ b/src/WeihanLi.Common/Template/EnvRenderMiddleware.cs
@@ -5,12 +5,16 @@ namespace WeihanLi.Common.Template;
internal sealed class EnvRenderMiddleware : IRenderMiddleware
{
- private const string Prefix = "$env ";
+ private const string Prefix = "$env";
public Task InvokeAsync(TemplateRenderContext context, Func next)
{
- foreach (var variable in context.Variables.Where(x => x.StartsWith(Prefix) && !context.Parameters.ContainsKey(x)))
+ foreach (var pair in context.Inputs
+ .Where(x => x.Key.Prefix is Prefix
+ && x.Value is null)
+ )
{
- context.Parameters[variable] = Environment.GetEnvironmentVariable(variable[Prefix.Length..]);
+ var variable = pair.Key.VariableName;
+ context.Inputs[pair.Key] = Environment.GetEnvironmentVariable(variable);
}
return next(context);
}
diff --git a/src/WeihanLi.Common/Template/ITemplatePipe.cs b/src/WeihanLi.Common/Template/ITemplatePipe.cs
new file mode 100644
index 00000000..ed766aee
--- /dev/null
+++ b/src/WeihanLi.Common/Template/ITemplatePipe.cs
@@ -0,0 +1,10 @@
+// Copyright (c) Weihan Li. All rights reserved.
+// Licensed under the Apache license.
+
+namespace WeihanLi.Common.Template;
+
+public interface ITemplatePipe
+{
+ string Name { get; }
+ object? Convert(object? value, params ReadOnlySpan args);
+}
diff --git a/src/WeihanLi.Common/Template/ITemplateRendererBuilder.cs b/src/WeihanLi.Common/Template/ITemplateRendererBuilder.cs
index ffaca99e..6643ab86 100644
--- a/src/WeihanLi.Common/Template/ITemplateRendererBuilder.cs
+++ b/src/WeihanLi.Common/Template/ITemplateRendererBuilder.cs
@@ -5,6 +5,9 @@ namespace WeihanLi.Common.Template;
public interface ITemplateRendererBuilder
{
+ ITemplateRendererBuilder UseTemplatePipe(TPipe pipe)
+ where TPipe : class, ITemplatePipe;
+
ITemplateRendererBuilder UseRenderMiddleware(TMiddleware middleware)
where TMiddleware : class, IRenderMiddleware;
}
diff --git a/src/WeihanLi.Common/Template/TemplateEngine.cs b/src/WeihanLi.Common/Template/TemplateEngine.cs
index 5bad2148..e63e7165 100644
--- a/src/WeihanLi.Common/Template/TemplateEngine.cs
+++ b/src/WeihanLi.Common/Template/TemplateEngine.cs
@@ -27,6 +27,10 @@ public async Task RenderAsync(string text, object? parameters = null)
public static TemplateEngine CreateDefault(Action? builderConfigure = null)
{
var builder = new TemplateEngineBuilder();
+ builder.UseTemplatePipe(new TextFormatTemplatePipe());
+ builder.UseTemplatePipe(new UpperCaseTemplatePipe());
+ builder.UseTemplatePipe(new LowerCaseTemplatePipe());
+ builder.UseTemplatePipe(new TitleCaseTemplatePipe());
builderConfigure?.Invoke(builder);
return new TemplateEngine(builder.BuildParser(), builder.BuildRenderer());
}
diff --git a/src/WeihanLi.Common/Template/TemplateEngineOptions.cs b/src/WeihanLi.Common/Template/TemplateEngineOptions.cs
index 1db2d0cf..d7897dbe 100644
--- a/src/WeihanLi.Common/Template/TemplateEngineOptions.cs
+++ b/src/WeihanLi.Common/Template/TemplateEngineOptions.cs
@@ -7,4 +7,6 @@ namespace WeihanLi.Common.Template;
public sealed class TemplateEngineOptions
{
public IConfiguration? Configuration { get; set; }
+
+ internal Dictionary Pipes { get; init; } = new();
}
diff --git a/src/WeihanLi.Common/Template/TemplateEngineServiceBuilder.cs b/src/WeihanLi.Common/Template/TemplateEngineServiceBuilder.cs
new file mode 100644
index 00000000..8e67d33e
--- /dev/null
+++ b/src/WeihanLi.Common/Template/TemplateEngineServiceBuilder.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Weihan Li. All rights reserved.
+// Licensed under the Apache license.
+
+using Microsoft.Extensions.DependencyInjection;
+
+namespace WeihanLi.Common.Template;
+
+public interface ITemplateEngineServiceBuilder
+{
+ IServiceCollection Services { get; }
+}
+
+internal sealed class TemplateEngineServiceBuilder(IServiceCollection services)
+ : ITemplateEngineServiceBuilder
+{
+ public IServiceCollection Services { get; } = services;
+}
diff --git a/src/WeihanLi.Common/Template/TemplatePipes.cs b/src/WeihanLi.Common/Template/TemplatePipes.cs
new file mode 100644
index 00000000..4da864da
--- /dev/null
+++ b/src/WeihanLi.Common/Template/TemplatePipes.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Weihan Li. All rights reserved.
+// Licensed under the Apache license.
+
+using System.Globalization;
+using WeihanLi.Extensions;
+
+namespace WeihanLi.Common.Template;
+
+public abstract class TemplatePipeBase : ITemplatePipe
+{
+ protected virtual int? ParameterCount => 1;
+
+ public abstract string Name { get; }
+ public object? Convert(object? value, params ReadOnlySpan args)
+ {
+ if (ParameterCount.HasValue && ParameterCount.Value != args.Length)
+ {
+ throw new InvalidOperationException($"The number of arguments {args.Length} must be equal to the parameter count({ParameterCount}).");
+ }
+
+ return ConvertInternal(value, args);
+ }
+
+ protected abstract object? ConvertInternal(object? value, params ReadOnlySpan args);
+}
+
+public sealed class TextFormatTemplatePipe : TemplatePipeBase
+{
+ protected override object? ConvertInternal(object? value, params ReadOnlySpan args)
+ => FormatText(value, args[0]);
+
+ public override string Name => "format";
+ private string? FormatText(object? value, string format)
+ {
+ return value switch
+ {
+ null => null,
+ IFormattable text => text.ToString(format, CultureInfo.InvariantCulture),
+ _ => value.ToString()
+ };
+ }
+}
+
+public abstract class TextTransformTemplatePipe : TemplatePipeBase
+{
+ protected override int? ParameterCount => 0;
+
+ protected override object? ConvertInternal(object? value, params ReadOnlySpan args)
+ {
+ var str = value as string ?? value?.ToString();
+ return str is null ? null : ConvertText(str);
+ }
+
+ protected abstract string? ConvertText(string value);
+}
+
+public sealed class UpperCaseTemplatePipe : TextTransformTemplatePipe
+{
+ public override string Name => "toUpper";
+ protected override string ConvertText(string value)
+ {
+ return value.ToUpperInvariant();
+ }
+}
+
+public sealed class LowerCaseTemplatePipe : TextTransformTemplatePipe
+{
+ public override string Name => "toLower";
+ protected override string ConvertText(string value)
+ {
+ return value.ToLowerInvariant();
+ }
+}
+
+public sealed class TitleCaseTemplatePipe : TextTransformTemplatePipe
+{
+ public override string Name => "toTitle";
+ protected override string ConvertText(string value)
+ {
+ return value.ToTitleCase();
+ }
+}
diff --git a/src/WeihanLi.Common/Template/TemplateRenderContext.cs b/src/WeihanLi.Common/Template/TemplateRenderContext.cs
index 09032696..b1f1cdd5 100644
--- a/src/WeihanLi.Common/Template/TemplateRenderContext.cs
+++ b/src/WeihanLi.Common/Template/TemplateRenderContext.cs
@@ -1,14 +1,36 @@
// Copyright (c) Weihan Li. All rights reserved.
// Licensed under the Apache license.
+using System.Diagnostics;
using WeihanLi.Common.Abstractions;
namespace WeihanLi.Common.Template;
-public sealed class TemplateRenderContext(string text, HashSet variables) : IProperties
+
+public sealed class TemplateRenderContext(string text, IReadOnlyCollection inputs)
+ : IProperties
{
public string Text { get; } = text;
- public HashSet Variables { get; } = variables;
+ public Dictionary Inputs { get; } =
+ inputs.ToDictionary(x=> x, _ => (object?)null);
public string RenderedText { get; set; } = text;
- public IDictionary Parameters { get; set; } = new Dictionary();
public IDictionary Properties { get; } = new Dictionary();
}
+
+[DebuggerDisplay("{Input,nq}")]
+public sealed class TemplateInput : IEquatable
+{
+ public required string Input { get; init; }
+ public required string? Prefix { get; init; }
+ public required string VariableName { get; init; }
+ public required TemplatePipeInput[] Pipes { get; init; }
+ public bool Equals(TemplateInput? other) => other is not null && other.Input == Input;
+ public override bool Equals(object? obj) => obj is TemplateInput input && Equals(input);
+ public override int GetHashCode() => Input.GetHashCode();
+}
+
+[DebuggerDisplay("{PipeName,nq}")]
+public sealed class TemplatePipeInput
+{
+ public required string PipeName { get; init; }
+ public required string[] Arguments { get; init; }
+}
diff --git a/src/WeihanLi.Common/Template/TemplateRendererBuilder.cs b/src/WeihanLi.Common/Template/TemplateRendererBuilder.cs
index 43455c37..a2516e42 100644
--- a/src/WeihanLi.Common/Template/TemplateRendererBuilder.cs
+++ b/src/WeihanLi.Common/Template/TemplateRendererBuilder.cs
@@ -10,6 +10,13 @@ internal sealed class TemplateEngineBuilder : ITemplateEngineBuilder
private readonly IAsyncPipelineBuilder _pipelineBuilder =
PipelineBuilder.CreateAsync();
private Action? _optionsConfigure;
+ private readonly Dictionary _pipes = new();
+
+ public ITemplateRendererBuilder UseTemplatePipe(TPipe pipe) where TPipe : class, ITemplatePipe
+ {
+ _pipes[pipe.Name] = pipe;
+ return this;
+ }
public ITemplateRendererBuilder UseRenderMiddleware(TMiddleware middleware) where TMiddleware : class, IRenderMiddleware
{
@@ -27,10 +34,14 @@ public ITemplateRendererBuilder ConfigureOptions(Action o
public ITemplateRenderer BuildRenderer()
{
- var options = new TemplateEngineOptions();
+ var options = new TemplateEngineOptions
+ {
+ Pipes = _pipes
+ };
_optionsConfigure?.Invoke(options);
+
_pipelineBuilder
- .UseMiddleware(new DefaultRenderMiddleware())
+ .UseMiddleware(new DefaultRenderMiddleware(options.Pipes))
.UseMiddleware(new EnvRenderMiddleware())
.UseMiddleware(new ConfigurationRenderMiddleware(options.Configuration))
;
diff --git a/test/WeihanLi.Common.Test/TemplateTest/TemplateParserTest.cs b/test/WeihanLi.Common.Test/TemplateTest/TemplateParserTest.cs
index eaeed566..53d7d764 100644
--- a/test/WeihanLi.Common.Test/TemplateTest/TemplateParserTest.cs
+++ b/test/WeihanLi.Common.Test/TemplateTest/TemplateParserTest.cs
@@ -8,6 +8,23 @@ namespace WeihanLi.Common.Test.TemplateTest;
public class TemplateParserTest
{
+ [Fact]
+ public async Task PipeParseTest()
+ {
+ var text = "Hello {{Name | upper}}";
+ var parser = new DefaultTemplateParser();
+ var context = await parser.ParseAsync(text);
+ Assert.Single(context.Inputs);
+ Assert.Single(context.Inputs.Keys.First().Pipes);
+ Assert.Single(context.Inputs.Keys.First().Pipes);
+ var input = context.Inputs.Keys.First().Input;
+ var pipe = context.Inputs.Keys.First().Pipes.First();
+ Assert.Equal("{{Name | upper}}", input);
+ Assert.Equal("upper", pipe.PipeName);
+ Assert.NotNull(pipe.Arguments);
+ Assert.Empty(pipe.Arguments);
+ }
+
[Fact]
public async Task ParseTest()
{
@@ -15,18 +32,36 @@ public async Task ParseTest()
Build Status: {{Status}}
Chart: {{$env CHART_NAME}} - {{$env VERSION}}
AppVersion: {{$env APP_VERSION}}
+ HostEnv: {{$env HOST | toUpper }}
Config: {{$config AppSettings:Host}}
Config: {{$config AppSettings--Host}}
Config: {{$config AppSettings__Host}}
""";
var result = await new DefaultTemplateParser()
.ParseAsync(template);
- Assert.Equal(6, result.Variables.Count);
- Assert.Contains("Status", result.Variables);
- Assert.Contains("$env CHART_NAME", result.Variables);
- Assert.Contains("$env VERSION", result.Variables);
- Assert.Contains("$env APP_VERSION", result.Variables);
- Assert.Contains("$config AppSettings:Host", result.Variables);
- Assert.Contains("$config AppSettings__Host", result.Variables);
+ var inputs = result.Inputs.Select(x => x.Key.Input)
+ .ToHashSet();
+ Assert.Equal(7, result.Inputs.Count);
+ Assert.Contains("{{Status}}", inputs);
+ Assert.Contains("{{$env CHART_NAME}}", inputs);
+ Assert.Contains("{{$env VERSION}}", inputs);
+ Assert.Contains("{{$env APP_VERSION}}", inputs);
+ Assert.Contains("{{$env HOST | toUpper }}", inputs);
+ Assert.Contains("{{$config AppSettings:Host}}", inputs);
+ Assert.Contains("{{$config AppSettings__Host}}", inputs);
+
+ var variableNames = result.Inputs.Select(x => x.Key.VariableName).ToHashSet();
+ Assert.Contains("Status", variableNames);
+ Assert.Contains("CHART_NAME", variableNames);
+ Assert.Contains("VERSION", variableNames);
+ Assert.Contains("APP_VERSION", variableNames);
+ Assert.Contains("AppSettings:Host", variableNames);
+ Assert.Contains("AppSettings__Host", variableNames);
+ Assert.Contains("HOST", variableNames);
+
+ var pipes = result.Inputs.SelectMany(x => x.Key.Pipes)
+ .Select(x=> x.PipeName)
+ .ToArray();
+ Assert.Contains("toUpper", pipes);
}
}
diff --git a/test/WeihanLi.Common.Test/TemplateTest/TemplateRendererTest.cs b/test/WeihanLi.Common.Test/TemplateTest/TemplateRendererTest.cs
new file mode 100644
index 00000000..93c2a971
--- /dev/null
+++ b/test/WeihanLi.Common.Test/TemplateTest/TemplateRendererTest.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Weihan Li. All rights reserved.
+// Licensed under the Apache license.
+
+using Microsoft.Extensions.Configuration;
+using WeihanLi.Common.Template;
+using WeihanLi.Extensions;
+using Xunit;
+
+namespace WeihanLi.Common.Test.TemplateTest;
+
+public class TemplateRendererTest
+{
+ private readonly TemplateEngine _templateEngine = TemplateEngine.CreateDefault(builder =>
+ {
+ builder.ConfigureOptions(options =>
+ {
+ options.Configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(
+ [
+ new KeyValuePair("Name", "test")
+ ])
+ .Build();
+ });
+ builder.UseTemplatePipe(new SubstringTemplatePipe());
+ });
+
+ [Fact]
+ public async Task VariableRenderTest()
+ {
+ var name = "mike";
+ var text = "Hello {{ Name | toTitle }}";
+ var renderedText = await _templateEngine.RenderAsync(text, new { Name = name });
+ Assert.Equal($"Hello {name.ToTitleCase()}", renderedText);
+ }
+
+ [Fact]
+ public async Task ConfigRenderTest()
+ {
+ var text = "Hello {{ $config Name | toTitle }}";
+ var renderedText = await _templateEngine.RenderAsync(text, new { Name = "mike" });
+ Assert.Equal($"Hello {"test".ToTitleCase()}", renderedText);
+ }
+
+ [Fact]
+ public async Task EnvRenderTest()
+ {
+ var text = "Hello {{$env hostname}}";
+ var renderedText = await _templateEngine.RenderAsync(text);
+ Assert.Equal($"Hello {Environment.GetEnvironmentVariable("hostname")}", renderedText);
+ }
+
+ [Fact]
+ public async Task CustomPipeRenderTest()
+ {
+ var name = "mike";
+ var text = "Hello {{ Name | substr:2 | toTitle }}";
+ var renderedText = await _templateEngine.RenderAsync(text, new { Name = name });
+ Assert.Equal($"Hello {name[2..].ToTitleCase()}", renderedText);
+ }
+}
+
+file sealed class SubstringTemplatePipe : TemplatePipeBase
+{
+ protected override int? ParameterCount => null;
+ public override string Name => "substr";
+ protected override string? ConvertInternal(object? value, params ReadOnlySpan args)
+ {
+ if (args.Length is not 1 or 2)
+ {
+ throw new InvalidOperationException("Arguments count must be 1 or 2");
+ }
+
+ var str = value as string ?? value?.ToString() ?? string.Empty;
+ var start = int.Parse(args[0]);
+ if (args.Length is 1)
+ {
+ return str[start..];
+ }
+ var len = int.Parse(args[1]);
+ return str[start..len];
+ }
+}