diff --git a/backend/FwLite/LcmCrdt.Tests/Changes/ChangeSerializationTests.cs b/backend/FwLite/LcmCrdt.Tests/Changes/ChangeSerializationTests.cs new file mode 100644 index 000000000..287c32950 --- /dev/null +++ b/backend/FwLite/LcmCrdt.Tests/Changes/ChangeSerializationTests.cs @@ -0,0 +1,82 @@ +using System.Reflection; +using System.Text.Json; +using FluentAssertions.Execution; +using LcmCrdt.Changes; +using LcmCrdt.Changes.Entries; +using MiniLcm.Tests.AutoFakerHelpers; +using SIL.Harmony.Changes; +using Soenneker.Utils.AutoBogus; +using SystemTextJsonPatch; + +namespace LcmCrdt.Tests.Changes; + +public class ChangeSerializationTests +{ + private static readonly AutoFaker Faker = new() + { + Config = + { + Overrides = [new WritingSystemIdOverride()] + } + }; + + public static IEnumerable Changes() + { + foreach (var type in LcmCrdtKernel.AllChangeTypes()) + { + //can't generate this type because there's no public constructor, so its specified below + if (type == typeof(SetComplexFormComponentChange)) continue; + + object change; + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(JsonPatchChange<>)) + { + change = PatchMethod.MakeGenericMethod(type.GenericTypeArguments[0]).Invoke(null, null)!; + } + else + { + change = Faker.Generate(type); + } + change.Should().NotBeNull($"change type {type.Name} should have been generated"); + yield return [change]; + } + yield return [SetComplexFormComponentChange.NewComplexForm(Guid.NewGuid(), Guid.NewGuid())]; + yield return [SetComplexFormComponentChange.NewComponent(Guid.NewGuid(), Guid.NewGuid())]; + yield return [SetComplexFormComponentChange.NewComponentSense(Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid())]; + } + + private static readonly MethodInfo PatchMethod = new Func(Patch).Method.GetGenericMethodDefinition(); + + private static IChange Patch() where T : class + { + return new JsonPatchChange(Guid.NewGuid(), new JsonPatchDocument()); + } + + [Theory] + [MemberData(nameof(Changes))] + public void CanRoundTripChanges(IChange change) + { + var config = new CrdtConfig(); + LcmCrdtKernel.ConfigureCrdt(config); + //commit id is not serialized + change.CommitId = Guid.Empty; + var type = change.GetType(); + var json = JsonSerializer.Serialize(change, config.JsonSerializerOptions); + var newChange = JsonSerializer.Deserialize(json, type, config.JsonSerializerOptions); + newChange.Should().BeEquivalentTo(change); + } + + [Fact] + public void ChangesIncludesAllValidChangeTypes() + { + var allChangeTypes = LcmCrdtKernel.AllChangeTypes(); + allChangeTypes.Should().NotBeEmpty(); + var testedTypes = Changes().Select(c => c[0].GetType()).ToArray(); + using (new AssertionScope()) + { + foreach (var allChangeType in allChangeTypes) + { + testedTypes.Should().Contain(allChangeType); + } + } + } +} diff --git a/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs b/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs index 1ce5a0321..dcc249a14 100644 --- a/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs +++ b/backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using MiniLcm; using MiniLcm.Models; using Xunit.Abstractions; @@ -17,6 +18,7 @@ public class MiniLcmApiFixture : IAsyncLifetime private LcmCrdtDbContext? _crdtDbContext; public CrdtMiniLcmApi Api => (CrdtMiniLcmApi)_services.ServiceProvider.GetRequiredService(); public DataModel DataModel => _services.ServiceProvider.GetRequiredService(); + public CrdtConfig CrdtConfig => _services.ServiceProvider.GetRequiredService>().Value; public MiniLcmApiFixture() { diff --git a/backend/FwLite/LcmCrdt/Changes/AddSemanticDomainChange.cs b/backend/FwLite/LcmCrdt/Changes/AddSemanticDomainChange.cs index 42a8c3a7c..e47f621a4 100644 --- a/backend/FwLite/LcmCrdt/Changes/AddSemanticDomainChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/AddSemanticDomainChange.cs @@ -3,8 +3,8 @@ namespace LcmCrdt.Changes; -public class AddSemanticDomainChange(SemanticDomain semanticDomain, Guid senseId) - : EditChange(senseId), ISelfNamedType +public class AddSemanticDomainChange(SemanticDomain semanticDomain, Guid entityId) + : EditChange(entityId), ISelfNamedType { public SemanticDomain SemanticDomain { get; } = semanticDomain; diff --git a/backend/FwLite/LcmCrdt/Changes/Entries/AddEntryComponentChange.cs b/backend/FwLite/LcmCrdt/Changes/Entries/AddEntryComponentChange.cs index f93630058..f12bb9d85 100644 --- a/backend/FwLite/LcmCrdt/Changes/Entries/AddEntryComponentChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/Entries/AddEntryComponentChange.cs @@ -16,9 +16,7 @@ public class AddEntryComponentChange : CreateChange, ISelf [JsonConstructor] public AddEntryComponentChange(Guid entityId, Guid complexFormEntryId, - string? complexFormHeadword, Guid componentEntryId, - string? componentHeadword, Guid? componentSenseId = null) : base(entityId) { ComplexFormEntryId = complexFormEntryId; @@ -28,9 +26,7 @@ public AddEntryComponentChange(Guid entityId, public AddEntryComponentChange(ComplexFormComponent component) : this(component.Id == default ? Guid.NewGuid() : component.Id, component.ComplexFormEntryId, - component.ComplexFormHeadword, component.ComponentEntryId, - component.ComponentHeadword, component.ComponentSenseId) { } diff --git a/backend/FwLite/LcmCrdt/Changes/RemoveSemanticDomainChange.cs b/backend/FwLite/LcmCrdt/Changes/RemoveSemanticDomainChange.cs index 80959cdd3..544c36d68 100644 --- a/backend/FwLite/LcmCrdt/Changes/RemoveSemanticDomainChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/RemoveSemanticDomainChange.cs @@ -3,8 +3,8 @@ namespace LcmCrdt.Changes; -public class RemoveSemanticDomainChange(Guid semanticDomainId, Guid senseId) - : EditChange(senseId), ISelfNamedType +public class RemoveSemanticDomainChange(Guid semanticDomainId, Guid entityId) + : EditChange(entityId), ISelfNamedType { public Guid SemanticDomainId { get; } = semanticDomainId; diff --git a/backend/FwLite/LcmCrdt/Changes/ReplaceSemanticDomainChange.cs b/backend/FwLite/LcmCrdt/Changes/ReplaceSemanticDomainChange.cs index f87e90afc..429bdc71b 100644 --- a/backend/FwLite/LcmCrdt/Changes/ReplaceSemanticDomainChange.cs +++ b/backend/FwLite/LcmCrdt/Changes/ReplaceSemanticDomainChange.cs @@ -3,8 +3,8 @@ namespace LcmCrdt.Changes; -public class ReplaceSemanticDomainChange(Guid oldSemanticDomainId, SemanticDomain semanticDomain, Guid senseId) - : EditChange(senseId), ISelfNamedType +public class ReplaceSemanticDomainChange(Guid oldSemanticDomainId, SemanticDomain semanticDomain, Guid entityId) + : EditChange(entityId), ISelfNamedType { public Guid OldSemanticDomainId { get; } = oldSemanticDomainId; public SemanticDomain SemanticDomain { get; } = semanticDomain; diff --git a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs index 08557d6bc..9b1495579 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtKernel.cs @@ -1,5 +1,7 @@ using System.Linq.Expressions; +using System.Reflection; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using SIL.Harmony; using SIL.Harmony.Core; using SIL.Harmony.Changes; @@ -191,6 +193,18 @@ public static void ConfigureCrdt(CrdtConfig config) .Add(); } + public static Type[] AllChangeTypes() + { + var crdtConfig = new CrdtConfig(); + ConfigureCrdt(crdtConfig); + + + var list = typeof(ChangeTypeListBuilder).GetProperty("Types", BindingFlags.Instance | BindingFlags.NonPublic) + ?.GetValue(crdtConfig.ChangeTypeListBuilder) as List; + return list?.Select(t => t.DerivedType).ToArray() ?? []; + } + + public static Task OpenCrdtProject(this IServiceProvider services, CrdtProject project) { //this method must not be async, otherwise Setting the project scope will not work as expected.