Skip to content
Open
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
6 changes: 3 additions & 3 deletions backend/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@
<PackageVersion Include="SIL.Chorus.LibChorus" Version="6.0.0-beta0064" />
<PackageVersion Include="SIL.Chorus.Mercurial" Version="6.5.1.43" />
<PackageVersion Include="SIL.ChorusPlugin.LfMergeBridge" Version="4.3.0-beta0048" />
<PackageVersion Include="SIL.Harmony" Version="0.2.1-rc.225" />
<PackageVersion Include="SIL.Harmony.Core" Version="0.2.1-rc.225" />
<PackageVersion Include="SIL.Harmony.Linq2db" Version="0.2.1-rc.225" />
<PackageVersion Include="SIL.Harmony" Version="0.2.1-rc.234" />
<PackageVersion Include="SIL.Harmony.Core" Version="0.2.1-rc.234" />
<PackageVersion Include="SIL.Harmony.Linq2db" Version="0.2.1-rc.234" />
<PackageVersion Include="SIL.Core" Version="17.0.0" />
<PackageVersion Include="SIL.LCModel" Version="11.0.0-beta0165" />
<PackageVersion Include="SIL.LCModel.FixData" Version="11.0.0-beta0165" />
Expand Down
3 changes: 2 additions & 1 deletion backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SIL.Harmony.Config;
using FwLiteMaui.Services;
using FwLiteShared;
using FwLiteShared.Auth;
Expand Down Expand Up @@ -116,7 +117,7 @@ public static void AddFwLiteMauiServices(this IServiceCollection services,
config.CacheFileName = fwLiteMauiConfig.AuthCacheFilePath;
config.SystemWebViewLogin = true;
});
services.Configure<CrdtConfig>(config =>
services.Configure<HarmonyConfig>(config =>
{
config.FailedSyncOutputPath = Path.Combine(baseDataPath, "failedSyncs");
config.LocalResourceCachePath = Path.Combine(baseDataPath, "localResourcesCache");
Expand Down
9 changes: 5 additions & 4 deletions backend/FwLite/FwLiteProjectSync/ProjectSnapshotService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SIL.Harmony.Config;
using System.Text.Json;
using FwDataMiniLcmBridge;
using LcmCrdt;
Expand All @@ -7,17 +8,17 @@

namespace FwLiteProjectSync;

public class ProjectSnapshotService(IOptions<CrdtConfig> crdtConfig)
public class ProjectSnapshotService(IOptions<HarmonyConfig> harmonyConfig)
{
public virtual async Task<ProjectSnapshot?> GetProjectSnapshot(FwDataProject project)
{
var snapshotPath = SnapshotPath(project);
if (!File.Exists(snapshotPath)) return null;
await using var file = File.OpenRead(snapshotPath);
// crdtConfig's options are fine for reading even though they "exclude" [MiniLcmInternal] members:
// harmonyConfig's options are fine for reading even though they "exclude" [MiniLcmInternal] members:
// the modifier only nulls the getter (the write side), so deserialization still populates those
// members (Order, entity Ids) via their setters. See SaveProjectSnapshot for why they're written.
return await JsonSerializer.DeserializeAsync<ProjectSnapshot>(file, crdtConfig.Value.JsonSerializerOptions);
return await JsonSerializer.DeserializeAsync<ProjectSnapshot>(file, harmonyConfig.Value.JsonSerializerOptions);
}

public virtual async Task RegenerateProjectSnapshot(IMiniLcmReadApi crdtApi, FwDataProject project, bool keepBackup)
Expand Down Expand Up @@ -52,7 +53,7 @@ internal static async Task SaveProjectSnapshot(FwDataProject project, ProjectSna
}

await using var file = File.Create(snapshotPath);
// Serialize with default options, not crdtConfig's: the CRDT options hide [MiniLcmInternal] members
// Serialize with default options, not harmonyConfig's: the CRDT options hide [MiniLcmInternal] members
// (the internal Order values and entity Ids) — that's the API's presentation view, which omits
// bookkeeping callers don't need. The snapshot is a stored record, so we keep the full object graph.
// The sync diff itself keys off business fields and list order, not these, so this is about a
Expand Down
5 changes: 3 additions & 2 deletions backend/FwLite/FwLiteShared/FwLiteSharedKernel.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SIL.Harmony.Config;
using System.Net;
using FwLiteShared.AppUpdate;
using FwLiteShared.Auth;
Expand Down Expand Up @@ -56,8 +57,8 @@ public static IServiceCollection AddFwLiteShared(this IServiceCollection service
services.AddOptions<FwLiteConfig>().BindConfiguration("FwLite");
services.DecorateConstructor<IJSRuntime>((provider, runtime) =>
{
var crdtConfig = provider.GetRequiredService<IOptions<CrdtConfig>>().Value;
runtime.ConfigureJsonSerializerOptions(crdtConfig);
var harmonyConfig = provider.GetRequiredService<IOptions<HarmonyConfig>>().Value;
runtime.ConfigureJsonSerializerOptions(harmonyConfig);
});
return services;
}
Expand Down
5 changes: 3 additions & 2 deletions backend/FwLite/FwLiteShared/Services/FwLiteJson.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SIL.Harmony.Config;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
Expand All @@ -12,7 +13,7 @@ public static class FwLiteJson
private static readonly PropertyInfo? _jsRuntimeJsonOptionsProperty =
typeof(JSRuntime).GetProperty("JsonSerializerOptions", BindingFlags.NonPublic | BindingFlags.Instance);

public static void ConfigureJsonSerializerOptions(this IJSRuntime jsRuntime, CrdtConfig crdtConfig)
public static void ConfigureJsonSerializerOptions(this IJSRuntime jsRuntime, HarmonyConfig harmonyConfig)
{
//this is not supported, see: https://github.com/dotnet/aspnetcore/issues/12685
//doing it anyway, otherwise our serialization will be broken
Expand All @@ -21,7 +22,7 @@ public static void ConfigureJsonSerializerOptions(this IJSRuntime jsRuntime, Crd
var options = (JsonSerializerOptions?)_jsRuntimeJsonOptionsProperty.GetValue(jsRuntime, null);
if (options is null) throw new InvalidOperationException("JSRuntime.JsonSerializerOptions returned null");
options.TypeInfoResolver = (options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver())
.WithAddedModifier(crdtConfig.MakeJsonTypeModifier())
.WithAddedModifier(harmonyConfig.MakeJsonTypeModifier())
.AddExternalMiniLcmModifiers();
}
}
10 changes: 5 additions & 5 deletions backend/FwLite/FwLiteShared/TypeGen/ChangeTypesCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ namespace FwLiteShared.TypeGen;

/// <summary>
/// Emits a `ChangeType` string-literal union and a `knownChangeTypes` array built from the registered CRDT
/// change types (<see cref="LcmCrdtKernel.AllChangeTypes"/>) and each type's serialized <c>$type</c>
/// discriminator (<see cref="HistoryService.GetChangeTypeKeyFromType"/>). Attached to the <c>ChangeType</c>
/// marker; suppresses the marker's own output.
/// change types and each type's serialized <c>$type</c> discriminator, taken straight from Harmony's
/// registration (<see cref="LcmCrdtKernel.AllRegisteredChanges"/>) so the generated list can't drift from
/// what the serializer writes. Attached to the <c>ChangeType</c> marker; suppresses the marker's own output.
/// </summary>
public class ChangeTypesCodeGenerator : ClassCodeGenerator
{
public override RtClass GenerateNode(Type element, RtClass result, TypeResolver resolver)
{
var typeNames = LcmCrdtKernel.AllChangeTypes()
.Select(HistoryService.GetChangeTypeKeyFromType)
var typeNames = LcmCrdtKernel.AllRegisteredChanges()
.Select(c => c.Discriminator)
.Distinct()
.OrderBy(name => name, StringComparer.Ordinal)
.ToArray();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SIL.Harmony.Config;
using System.Drawing;
using System.Reflection;
using System.Text.Json.Serialization;
Expand Down Expand Up @@ -80,7 +81,7 @@ private static void ConfigureMiniLcmTypes(ConfigurationBuilder builder)
builder.ExportAsThirdParty<RichMultiString>().WithName("IRichMultiString").Imports([
new() { From = "$lib/dotnet-types/i-multi-string", Target = "type {IRichMultiString}" }
]);
var config = new CrdtConfig();
var config = new HarmonyConfig();
LcmCrdtKernel.ConfigureCrdt(config);
builder.ExportAsInterfaces([
..config.ObjectTypes,
Expand Down
11 changes: 9 additions & 2 deletions backend/FwLite/FwLiteWeb/FwLiteWebKernel.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SIL.Harmony.Config;
using FwDataMiniLcmBridge;
using FwLiteProjectSync;
using SIL.Harmony;
Expand Down Expand Up @@ -29,9 +30,15 @@ public static IServiceCollection AddFwLiteWebServices(this IServiceCollection se
services.AddSingleton<IHostedService, NetworkChangeSyncTrigger>();
services.AddOptions<FwLiteWebConfig>().BindConfiguration("FwLiteWeb");

services.AddOptions<JsonOptions>().PostConfigure<IOptions<CrdtConfig>>((jsonOptions, crdtConfig) =>
services.AddOptions<JsonOptions>().PostConfigure<IOptions<HarmonyConfig>>((jsonOptions, harmonyConfig) =>
{
jsonOptions.SerializerOptions.TypeInfoResolver = crdtConfig.Value.MakeLcmCrdtExternalJsonTypeResolver();
// Copy both the type resolver and Harmony's converters: IChange polymorphism now lives in an
// internal converter (see MakeLcmCrdtExternalJsonOptions), so the resolver alone can't decode
// ChangeEntity<IChange> in sync payloads.
var externalOptions = harmonyConfig.Value.MakeLcmCrdtExternalJsonOptions();
jsonOptions.SerializerOptions.TypeInfoResolver = externalOptions.TypeInfoResolver;
foreach (var converter in externalOptions.Converters)
jsonOptions.SerializerOptions.Converters.Add(converter);
Comment on lines +38 to +41

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this should maybe happen inside or in a sibling method of MakeLcmCrdtExternalJsonOptions? It's not obvious here what things from MakeLcmCrdtExternalJsonOptions need to be copied over, but it is obvious where MakeLcmCrdtExternalJsonOptions is defined.

});
return services;
}
Expand Down
14 changes: 14 additions & 0 deletions backend/FwLite/LcmCrdt.Tests/Changes/ChangeSerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ public void CanRoundTripChanges(IChange change)
newChange.Should().BeEquivalentTo(change);
}

[Theory]
[MemberData(nameof(Changes))]
public void CanRoundTripChangesAsIChangeWithExternalOptions(IChange change)
{
// The sync client, web host, and debugger all (de)serialize change fields as the abstract IChange
// using the external (Web) options. That relies on Harmony's polymorphic converter, not just the
// type resolver — regression guard against the "Deserialization of interface or abstract types is
// not supported. Type 'IChange'" break when only the resolver was wired onto the external options.
var options = TestJsonOptions.External();
var json = JsonSerializer.Serialize(change, options);
var newChange = JsonSerializer.Deserialize<IChange>(json, options);
newChange.Should().BeEquivalentTo(change);
}

[Fact]
public void ChangesIncludesAllValidChangeTypes()
{
Expand Down
7 changes: 4 additions & 3 deletions backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SIL.Harmony.Config;
using FluentAssertions.Execution;
using LcmCrdt.Changes;
using LcmCrdt.Changes.Entries;
Expand All @@ -23,11 +24,11 @@ public class ConfigRegistrationTests
typeof(DeleteChange<RemoteResource<LcmFileMetadata>>)//Not used, instead DeleteRemoteResourceChange is used
];

private readonly CrdtConfig _config;
private readonly HarmonyConfig _config;

public ConfigRegistrationTests()
{
_config = new CrdtConfig();
_config = new HarmonyConfig();
LcmCrdtKernel.ConfigureCrdt(_config);
}

Expand Down Expand Up @@ -57,7 +58,7 @@ public void AllChangesAreRegistered()
.Append(typeof(DeleteChange<>))
.ToLookup(t => t.IsGenericType);
allChangeTypes.Should().NotBeEmpty();
var registeredChangeTypes = _config.ChangeTypes.ToHashSet();
var registeredChangeTypes = _config.ChangeTypes.Select(t => t.Type).ToHashSet();
using var _ = new AssertionScope();
//non generics
foreach (var changeType in allChangeTypes[false])
Expand Down
5 changes: 3 additions & 2 deletions backend/FwLite/LcmCrdt.Tests/Data/MigrationTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SIL.Harmony.Config;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json;
Expand Down Expand Up @@ -60,7 +61,7 @@ public async Task VerifyAfterMigrationFromScriptedDb(RegressionTestHelper.Regres
{
await _helper.InitializeAsync(regressionVersion);
var api = _helper.Services.GetRequiredService<IMiniLcmApi>();
var crdtConfig = _helper.Services.GetRequiredService<IOptions<CrdtConfig>>().Value;
var harmonyConfig = _helper.Services.GetRequiredService<IOptions<HarmonyConfig>>().Value;

await using var dbContext = await _helper.Services.GetRequiredService<ICrdtDbContextFactory>().CreateDbContextAsync();
var snapshots = await dbContext.Snapshots.AsNoTracking()
Expand Down Expand Up @@ -112,7 +113,7 @@ public async Task VerifyRegeneratedSnapshotsAfterMigrationFromScriptedDb(Regress
{
await _helper.InitializeAsync(regressionVersion);
var api = _helper.Services.GetRequiredService<IMiniLcmApi>();
var crdtConfig = _helper.Services.GetRequiredService<IOptions<CrdtConfig>>().Value;
var harmonyConfig = _helper.Services.GetRequiredService<IOptions<HarmonyConfig>>().Value;

await using var dbContext = await _helper.Services.GetRequiredService<ICrdtDbContextFactory>().CreateDbContextAsync();
await using var dataModel = _helper.Services.GetRequiredService<DataModel>();
Expand Down
12 changes: 8 additions & 4 deletions backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SIL.Harmony.Config;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using FluentAssertions.Execution;
Expand All @@ -20,7 +21,7 @@ public class DataModelSnapshotTests : IAsyncLifetime

protected readonly AsyncServiceScope _services;
private readonly LcmCrdtDbContext _crdtDbContext;
private CrdtConfig _crdtConfig;
private HarmonyConfig _crdtConfig;
private CrdtProject _crdtProject;
private readonly JsonSerializerOptions _jsonSerializerOptions = TestJsonOptions.Harmony();

Expand All @@ -33,7 +34,7 @@ public DataModelSnapshotTests()
.BuildServiceProvider();
_services = services.CreateAsyncScope();
_crdtDbContext = _services.ServiceProvider.GetRequiredService<IDbContextFactory<LcmCrdtDbContext>>().CreateDbContext();
_crdtConfig = _services.ServiceProvider.GetRequiredService<IOptions<CrdtConfig>>().Value;
_crdtConfig = _services.ServiceProvider.GetRequiredService<IOptions<HarmonyConfig>>().Value;
}

public async Task InitializeAsync()
Expand Down Expand Up @@ -65,7 +66,7 @@ private IEnumerable<JsonDerivedType> GetPolymorphicTypesFor(Type type)
{
var polymorphismOptions = _jsonSerializerOptions.GetTypeInfo(type)
.PolymorphismOptions;
polymorphismOptions.Should().NotBeNull("type {type} should only be called if it's configured properly", type);
polymorphismOptions.Should().NotBeNull("type {0} should only be called if it's configured properly", type);
polymorphismOptions.TypeDiscriminatorPropertyName.Should().Be("$type");
return polymorphismOptions.DerivedTypes.OrderBy(t => t.DerivedType.FullName);
}
Expand All @@ -74,7 +75,10 @@ private IEnumerable<JsonDerivedType> GetPolymorphicTypesFor(Type type)
[Trait("Category", "Verified")]
public async Task VerifyChangeModels()
{
await Verify(GetPolymorphicTypesFor(typeof(IChange)));
await Verify(LcmCrdtKernel.AllRegisteredChanges()
//map to JsonDerivedType to match the verification format previously used
.Select(c => new JsonDerivedType(c.Type, c.Discriminator))
.OrderBy(j => j.DerivedType.FullName));
}

[Fact]
Expand Down
7 changes: 4 additions & 3 deletions backend/FwLite/LcmCrdt.Tests/EntityCopyMethodTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SIL.Harmony.Config;
using FluentAssertions.Equivalency;
using FluentAssertions.Execution;
using MiniLcm.Media;
Expand All @@ -14,9 +15,9 @@ public class EntityCopyMethodTests

public static IEnumerable<object[]> GetEntityTypes()
{
var crdtConfig = new CrdtConfig();
LcmCrdtKernel.ConfigureCrdt(crdtConfig);
return crdtConfig.ObjectTypes
var harmonyConfig = new HarmonyConfig();
LcmCrdtKernel.ConfigureCrdt(harmonyConfig);
return harmonyConfig.ObjectTypes
.Except([typeof(RemoteResource<LcmFileMetadata>)])//exclude remote resource as it's a harmony defined type, not miniLcm
.Select(t => new object[] { t });
}
Expand Down
3 changes: 2 additions & 1 deletion backend/FwLite/LcmCrdt.Tests/MiniLcmApiFixture.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SIL.Harmony.Config;
using System.Diagnostics;
using LcmCrdt.MediaServer;
using LcmCrdt.Objects;
Expand All @@ -20,7 +21,7 @@ public class MiniLcmApiFixture : IAsyncLifetime, IAsyncDisposable
public CrdtMiniLcmApi Api => (CrdtMiniLcmApi)_services.ServiceProvider.GetRequiredService<IMiniLcmApi>();
public DataModel DataModel => _services.ServiceProvider.GetRequiredService<DataModel>();
public LcmCrdtDbContext DbContext => _crdtDbContext ?? throw new InvalidOperationException("MiniLcmApiFixture not initialized");
public CrdtConfig CrdtConfig => _services.ServiceProvider.GetRequiredService<IOptions<CrdtConfig>>().Value;
public HarmonyConfig HarmonyConfig => _services.ServiceProvider.GetRequiredService<IOptions<HarmonyConfig>>().Value;

public T GetService<T>() where T : notnull
{
Expand Down
12 changes: 6 additions & 6 deletions backend/FwLite/LcmCrdt.Tests/TestJsonOptions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SIL.Harmony.Config;
using System.Text.Json;

namespace LcmCrdt.Tests;
Expand All @@ -6,17 +7,16 @@ public static class TestJsonOptions
{
public static JsonSerializerOptions External()
{
var config = new CrdtConfig();
var config = new HarmonyConfig();
LcmCrdtKernel.ConfigureCrdt(config);
return new JsonSerializerOptions(JsonSerializerDefaults.General)
{
TypeInfoResolver = config.MakeLcmCrdtExternalJsonTypeResolver(),
};
// Full external options (resolver + Harmony's IChange converter), matching what the sync client,
// web host, and debugger use — a resolver alone can't deserialize IChange.
return config.MakeLcmCrdtExternalJsonOptions();
}

public static JsonSerializerOptions Harmony()
{
var config = new CrdtConfig();
var config = new HarmonyConfig();
LcmCrdtKernel.ConfigureCrdt(config);
return config.JsonSerializerOptions;
}
Expand Down
5 changes: 3 additions & 2 deletions backend/FwLite/LcmCrdt/CrdtProjectsService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SIL.Harmony.Config;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
Expand Down Expand Up @@ -209,7 +210,7 @@ public virtual async Task<CrdtProject> CreateProjectFromTemplate(
AfterCreate = async (provider, project) =>
{
var api = provider.GetRequiredService<IMiniLcmApi>();
var jsonOptions = provider.GetRequiredService<IOptions<CrdtConfig>>().Value.JsonSerializerOptions;
var jsonOptions = provider.GetRequiredService<IOptions<HarmonyConfig>>().Value.JsonSerializerOptions;
var snapshot = ProjectTemplate.CreateNewSnapshot(jsonOptions, vernacularWs, analysisWs);
var commitMetadataInterceptor = provider.GetRequiredService<CommitMetadataInterceptor>();
using (commitMetadataInterceptor.Intercept(CommitHelpers.StampAsTemplate))
Expand Down Expand Up @@ -344,7 +345,7 @@ private Task EnsureDeleteProject(string sqliteFile, bool suppressException = fal
public async Task DeleteProject(string code)
{
var project = GetProject(code) ?? throw new InvalidOperationException($"Project {code} not found");
var projectResourceCachePath = LcmMediaService.ProjectCachePath(project, provider.GetRequiredService<IOptions<CrdtConfig>>().Value);
var projectResourceCachePath = LcmMediaService.ProjectCachePath(project, provider.GetRequiredService<IOptions<HarmonyConfig>>().Value);
if (Directory.Exists(projectResourceCachePath)) Directory.Delete(projectResourceCachePath, true);
await EnsureDeleteProject(project.DbPath);
}
Expand Down
5 changes: 3 additions & 2 deletions backend/FwLite/LcmCrdt/Data/SetupCollationInterceptor.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SIL.Harmony.Config;
using System.Data;
using System.Data.Common;
using System.Globalization;
Expand All @@ -13,7 +14,7 @@

namespace LcmCrdt.Data;

public class SetupCollationInterceptor(IMemoryCache cache, IMiniLcmCultureProvider cultureProvider, IOptions<CrdtConfig> crdtConfig) : IDbConnectionInterceptor, ISaveChangesInterceptor, IConnectionInterceptor
public class SetupCollationInterceptor(IMemoryCache cache, IMiniLcmCultureProvider cultureProvider, IOptions<HarmonyConfig> harmonyConfig) : IDbConnectionInterceptor, ISaveChangesInterceptor, IConnectionInterceptor
{
private static string? WsTableName = null;
private WritingSystem[] GetWritingSystems(DbConnection connection, LcmCrdtDbContext? dbContext = null)
Expand All @@ -29,7 +30,7 @@ private WritingSystem[] GetWritingSystems(DbConnection connection, LcmCrdtDbCont
{
var optionsBuilder = new DbContextOptionsBuilder<LcmCrdtDbContext>();
optionsBuilder.UseSqlite(connection);
localContext = new LcmCrdtDbContext(optionsBuilder.Options, crdtConfig);
localContext = new LcmCrdtDbContext(optionsBuilder.Options, harmonyConfig);
}

try
Expand Down
Loading
Loading