Skip to content
Closed
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
23 changes: 23 additions & 0 deletions src/SIL.Harmony.Tests/ConfigTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Text.Json;
using SIL.Harmony.Changes;
using SIL.Harmony.Sample.Changes;
using SIL.Harmony.Sample.Models;
using SIL.Harmony.Tests.Adapter;
Expand Down Expand Up @@ -29,4 +31,25 @@ public void CanGetChangeTypes()
var types = config.ChangeTypes.ToArray();
types.Should().BeEquivalentTo([typeof(NewDefinitionChange), typeof(SetWordTextChange)]);
}

[Fact]
public void CanAddChangeTypesAfterReadingJsonSerializerOptions()
{
// Mirrors a consumer that reads JsonSerializerOptions mid-configuration (to layer its own
// TypeInfoResolver modifier onto ours) and then keeps registering change types.
var config = new CrdtConfig();
config.ChangeTypeListBuilder.Add<NewDefinitionChange>();

_ = config.JsonSerializerOptions;

config.ChangeTypeListBuilder.Add<SetWordTextChange>();

var options = config.JsonSerializerOptions;
var entityId = Guid.Parse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
var json = JsonSerializer.Serialize<IChange>(new SetWordTextChange(entityId, "hello"), options);

var roundTripped = JsonSerializer.Deserialize<IChange>(json, options);
roundTripped.Should().BeOfType<SetWordTextChange>()
.Which.Text.Should().Be("hello");
}
}
14 changes: 8 additions & 6 deletions src/SIL.Harmony/Changes/PeekThenConcreteChangeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ namespace SIL.Harmony.Changes;
/// </summary>
internal sealed class PeekThenConcreteChangeConverter : JsonConverter<IChange>
{
private readonly KnownType[] _known;
private readonly Lazy<KnownType[]> _known;
private readonly byte[] _discriminatorPropertyUtf8;

public PeekThenConcreteChangeConverter(IReadOnlyDictionary<string, Type> known)
public PeekThenConcreteChangeConverter(Func<IReadOnlyDictionary<string, Type>> knownFactory)
{
_discriminatorPropertyUtf8 = Encoding.UTF8.GetBytes(CrdtConstants.ChangeDiscriminatorProperty);
_known = known.Select(kv => new KnownType(Encoding.UTF8.GetBytes(kv.Key), kv.Value)).ToArray();
_known = new Lazy<KnownType[]>(() =>
knownFactory().Select(kv => new KnownType(Encoding.UTF8.GetBytes(kv.Key), kv.Value)).ToArray());
}

public override IChange Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
Expand All @@ -46,7 +47,7 @@ public override IChange Read(ref Utf8JsonReader reader, Type typeToConvert, Json
return ReadOpaque(ref reader, unknownTypeName!);
}

ref var known = ref _known[knownIndex];
ref var known = ref _known.Value[knownIndex];
var typeInfo = known.EnsureTypeInfo(options);

// Real change types use parameterized constructors / get-only props — let STJ materialize.
Expand Down Expand Up @@ -83,9 +84,10 @@ private static OpaqueChange ReadOpaque(ref Utf8JsonReader reader, string typeNam

private bool TryFindKnown(ref Utf8JsonReader reader, out int index, out string? unknownTypeName)
{
for (var i = 0; i < _known.Length; i++)
var known = _known.Value;
for (var i = 0; i < known.Length; i++)
{
if (reader.ValueTextEquals(_known[i].Utf8Discriminator))
if (reader.ValueTextEquals(known[i].Utf8Discriminator))
{
index = i;
unknownTypeName = null;
Expand Down
7 changes: 4 additions & 3 deletions src/SIL.Harmony/CrdtConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ public CrdtConfig()

private JsonSerializerOptions CreateJsonSerializerOptions()
{
var changeDiscriminators = _lazyChangeDiscriminatorMaps.Value;

var options = new JsonSerializerOptions(JsonSerializerDefaults.General)
{
TypeInfoResolver = MakeJsonTypeResolver()
};
options.Converters.Add(new PeekThenConcreteChangeConverter(changeDiscriminators.ByDiscriminator));
// A factory, not .Value: building the map freezes ChangeTypeListBuilder, but consumers read these
// options mid-configuration (to add their own TypeInfoResolver modifier) and keep registering
// change types. The converter pulls the map on first use, once registration is done.
options.Converters.Add(new PeekThenConcreteChangeConverter(() => _lazyChangeDiscriminatorMaps.Value.ByDiscriminator));
return options;
}

Expand Down
Loading