From e073f741238ea323034da50ee5f85926e89f938d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 09:21:49 +0000 Subject: [PATCH 1/2] Initial plan From c67a2281bf0b48296875de3100f9f00a37cefa15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 09:34:58 +0000 Subject: [PATCH 2/2] Add JsonSourceGenTests for System.Text.Json source generation support Co-authored-by: sebastienros <1165805+sebastienros@users.noreply.github.com> --- Fluid.Tests/JsonSourceGenTests.cs | 164 ++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 Fluid.Tests/JsonSourceGenTests.cs diff --git a/Fluid.Tests/JsonSourceGenTests.cs b/Fluid.Tests/JsonSourceGenTests.cs new file mode 100644 index 00000000..f72919ba --- /dev/null +++ b/Fluid.Tests/JsonSourceGenTests.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using Fluid; +using Fluid.Filters; +using Fluid.Values; +using Xunit; + +namespace Fluid.Tests +{ + [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] + [JsonSerializable(typeof(SourceGenPerson))] + [JsonSerializable(typeof(SourceGenAddress))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(string[]))] + public partial class FluidJsonContext : JsonSerializerContext { } + + public sealed class SourceGenPerson + { + public string FirstName { get; init; } = string.Empty; + public string LastName { get; init; } = string.Empty; + public int Age { get; init; } + public SourceGenAddress HomeAddress { get; init; } + public List Tags { get; init; } + public Dictionary Scores { get; init; } + } + + public sealed class SourceGenAddress + { + public string Street { get; init; } = string.Empty; + public string City { get; init; } = string.Empty; + } + + public class JsonSourceGenTests + { + private static readonly FluidParser _parser = new FluidParser(); + + private static IFluidTemplate Parse(string liquid) + { + Assert.True(_parser.TryParse(liquid, out var template, out var errors), errors); + return template; + } + + private static TemplateContext CreateContext(Action configureOptions = null) + { + var options = new TemplateOptions(); + configureOptions?.Invoke(options); + return new TemplateContext(options); + } + + [Fact] + public void JsonFilter_UsesSourceGeneratedContext_ForSimpleObject() + { + var person = new SourceGenPerson + { + FirstName = "John", + LastName = "Doe", + Age = 42, + HomeAddress = new SourceGenAddress { Street = "123 Main", City = "Metropolis" }, + Tags = new List { "admin", "author" }, + Scores = new Dictionary { ["math"] = 95, ["science"] = 90 } + }; + + var ctx = CreateContext(o => + { + var genOptions = new JsonSerializerOptions + { + TypeInfoResolver = JsonTypeInfoResolver.Combine(FluidJsonContext.Default, new DefaultJsonTypeInfoResolver()), + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + o.JsonSerializerOptions = genOptions; + }); + + ctx.Options.Filters.WithMiscFilters(); + ctx.SetValue("person", person); + var template = Parse("{{ person | json }}"); + var rendered = template.Render(ctx); + var direct = JsonSerializer.Serialize(person, FluidJsonContext.Default.SourceGenPerson); + Assert.Equal(direct, rendered); + } + + [Fact] + public void JsonFilter_UsesSourceGen_ForArraysAndDictionaries() + { + var ctx = CreateContext(o => + { + var genOptions = new JsonSerializerOptions + { + TypeInfoResolver = JsonTypeInfoResolver.Combine(FluidJsonContext.Default, new DefaultJsonTypeInfoResolver()) + }; + o.JsonSerializerOptions = genOptions; + }); + + ctx.Options.Filters.WithMiscFilters(); + var tags = new[] { "one", "two", "three" }; + var scores = new Dictionary { ["alpha"] = 1, ["beta"] = 2 }; + ctx.SetValue("tags", tags); + ctx.SetValue("scores", scores); + var template = Parse("Tags: {{ tags | json }} Scores: {{ scores | json }}"); + var rendered = template.Render(ctx); + var directTags = JsonSerializer.Serialize(tags, FluidJsonContext.Default.StringArray); + var directScores = JsonSerializer.Serialize(scores, FluidJsonContext.Default.DictionaryStringInt32); + Assert.Equal($"Tags: {directTags} Scores: {directScores}", rendered); + } + + [Fact] + public void JsonFilter_FluidValueWrapping_DoesNotBreakSourceGen() + { + var ctx = CreateContext(o => + { + var genOptions = new JsonSerializerOptions + { + TypeInfoResolver = JsonTypeInfoResolver.Combine(FluidJsonContext.Default, new DefaultJsonTypeInfoResolver()) + }; + o.JsonSerializerOptions = genOptions; + }); + + ctx.Options.Filters.WithMiscFilters(); + var fluidString = new StringValue("Hello SourceGen"); + ctx.SetValue("msg", fluidString); + var array = new object[] { "A", 123, true }; + ctx.SetValue("mixed", array); + var template = Parse("{{ msg | json }} {{ mixed | json }}"); + var rendered = template.Render(ctx); + var expectedMsg = JsonSerializer.Serialize("Hello SourceGen", FluidJsonContext.Default.String); + var expectedMixed = JsonSerializer.Serialize(array, ctx.JsonSerializerOptions); + Assert.Equal($"{expectedMsg} {expectedMixed}", rendered); + } + + [Fact] + public void JsonFilter_RespectsCamelCaseNamingPolicy_FromSourceGen() + { + var person = new SourceGenPerson + { + FirstName = "Jane", + LastName = "Roe", + Age = 30, + HomeAddress = new SourceGenAddress { Street = "500 Market", City = "Gotham" }, + Tags = new List { "x", "y" }, + Scores = new Dictionary { ["logic"] = 88 } + }; + + var ctx = CreateContext(o => + { + var genOptions = new JsonSerializerOptions + { + TypeInfoResolver = JsonTypeInfoResolver.Combine(FluidJsonContext.Default, new DefaultJsonTypeInfoResolver()), + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + o.JsonSerializerOptions = genOptions; + }); + + ctx.Options.Filters.WithMiscFilters(); + ctx.SetValue("person", person); + var template = Parse("{{ person | json }}"); + var output = template.Render(ctx); + Assert.Contains("\"firstName\":\"Jane\"", output); + Assert.Contains("\"homeAddress\":{\"street\":\"500 Market\",\"city\":\"Gotham\"}", output); + } + } +}