diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index ba783e1f6a..4cb01a69e3 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -85,28 +85,25 @@ internal int GetWritingSystemHandle(WritingSystemId ws, WritingSystemType? type public Task GetWritingSystems() { - var currentVernacularWs = WritingSystemContainer - .CurrentVernacularWritingSystems - .Select(ws => ws.Id).ToHashSet(); - var currentAnalysisWs = WritingSystemContainer - .CurrentAnalysisWritingSystems - .Select(ws => ws.Id).ToHashSet(); var writingSystems = new WritingSystems { Vernacular = WritingSystemContainer.CurrentVernacularWritingSystems.Select((definition, index) => - FromLcmWritingSystem(definition, index, WritingSystemType.Vernacular)).ToArray(), + FromLcmWritingSystem(definition, WritingSystemType.Vernacular, index)).ToArray(), Analysis = WritingSystemContainer.CurrentAnalysisWritingSystems.Select((definition, index) => - FromLcmWritingSystem(definition, index, WritingSystemType.Analysis)).ToArray() + FromLcmWritingSystem(definition, WritingSystemType.Analysis, index)).ToArray() }; - CompleteExemplars(writingSystems); + // Not used and not implemented in CRDT (also not done in GetWritingSystem()) + // CompleteExemplars(writingSystems); return Task.FromResult(writingSystems); } - private WritingSystem FromLcmWritingSystem(CoreWritingSystemDefinition ws, int index, WritingSystemType type) + private WritingSystem FromLcmWritingSystem(CoreWritingSystemDefinition ws, WritingSystemType type, int index = default) { return new WritingSystem { Id = Guid.Empty, + // todo: Order probably shouldn't be relied on in fwdata, because it's implicit, + // so it probably shouldn't be used or set at all Order = index, Type = type, //todo determine current and create a property for that. @@ -118,15 +115,12 @@ private WritingSystem FromLcmWritingSystem(CoreWritingSystemDefinition ws, int i }; } - public async Task GetWritingSystem(WritingSystemId id, WritingSystemType type) + public Task GetWritingSystem(WritingSystemId id, WritingSystemType type) { - var writingSystems = await GetWritingSystems(); - return type switch - { - WritingSystemType.Vernacular => writingSystems.Vernacular.FirstOrDefault(ws => ws.WsId == id), - WritingSystemType.Analysis => writingSystems.Analysis.FirstOrDefault(ws => ws.WsId == id), - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) - }; + var lcmWs = Cache.TryGetCoreWritingSystem(id, type); + if (lcmWs is null) return Task.FromResult(null); + var ws = FromLcmWritingSystem(lcmWs, type); + return Task.FromResult(ws); } internal void CompleteExemplars(WritingSystems writingSystems) @@ -187,7 +181,7 @@ await Cache.DoUsingNewOrCurrentUOW("Create Writing System", WritingSystemType.Vernacular => WritingSystemContainer.CurrentVernacularWritingSystems.Count, _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) } - 1; - return FromLcmWritingSystem(ws, index, type); + return FromLcmWritingSystem(ws, type, index); } public async Task UpdateWritingSystem(WritingSystemId id, WritingSystemType type, UpdateObjectInput update) @@ -224,10 +218,10 @@ await Cache.DoUsingNewOrCurrentUOW("Update WritingSystem", public async Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition between) { - var wsToUpdate = GetLexWritingSystem(id, type); + var wsToUpdate = GetNonDefaultLexWritingSystem(id, type); if (wsToUpdate is null) throw new NullReferenceException($"unable to find writing system with id {id}"); - var previousWs = between.Previous is null ? null : GetLexWritingSystem(between.Previous.Value, type); - var nextWs = between.Next is null ? null : GetLexWritingSystem(between.Next.Value, type); + var previousWs = between.Previous is null ? null : GetNonDefaultLexWritingSystem(between.Previous.Value, type); + var nextWs = between.Next is null ? null : GetNonDefaultLexWritingSystem(between.Next.Value, type); if (nextWs is null && previousWs is null) throw new NullReferenceException($"unable to find writing system with id {between.Previous} or {between.Next}"); await Cache.DoUsingNewOrCurrentUOW("Move WritingSystem", "Revert Move WritingSystem", @@ -269,12 +263,10 @@ void MoveWs(CoreWritingSystemDefinition ws, }); } - private CoreWritingSystemDefinition? GetLexWritingSystem(WritingSystemId id, WritingSystemType type) + private CoreWritingSystemDefinition? GetNonDefaultLexWritingSystem(WritingSystemId id, WritingSystemType type) { - var exitingWs = type == WritingSystemType.Analysis - ? WritingSystemContainer.AnalysisWritingSystems - : WritingSystemContainer.VernacularWritingSystems; - return exitingWs.FirstOrDefault(ws => ws.Id == id); + if (id == default) throw new ArgumentException("Cannot use default writing system ID", nameof(id)); + return Cache.TryGetCoreWritingSystem(id, type); } public IAsyncEnumerable GetPartsOfSpeech() diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs index 830c5d862b..b50e2bbdb9 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs @@ -166,42 +166,44 @@ internal static WritingSystemId GetWritingSystemId(this LcmCache cache, int ws) return cache.ServiceLocator.WritingSystemManager.Get(ws).Id; } - internal static int GetWritingSystemHandle(this LcmCache cache, WritingSystemId ws, WritingSystemType? type = null) + internal static CoreWritingSystemDefinition? TryGetCoreWritingSystem(this LcmCache cache, WritingSystemId wsId, WritingSystemType type) { var wsContainer = cache.ServiceLocator.WritingSystems; - if (ws == "default") + var writingSystemsOfType = type switch { - return type switch - { - WritingSystemType.Analysis => wsContainer.DefaultAnalysisWritingSystem.Handle, - WritingSystemType.Vernacular => wsContainer.DefaultVernacularWritingSystem.Handle, - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) - }; - } + WritingSystemType.Analysis => wsContainer.AnalysisWritingSystems, + WritingSystemType.Vernacular => wsContainer.VernacularWritingSystems, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + if (wsId == default) return writingSystemsOfType.FirstOrDefault(); + return writingSystemsOfType.FirstOrDefault(ws => ws.Id == wsId); + } - if (!cache.ServiceLocator.WritingSystemManager.TryGet(ws.Code, out var lcmWs)) + internal static CoreWritingSystemDefinition GetCoreWritingSystem(this LcmCache cache, WritingSystemId wsId, WritingSystemType? type = null) + { + if (type is not null) { - throw new NullReferenceException($"unable to find writing system with id '{ws.Code}'"); + return TryGetCoreWritingSystem(cache, wsId, type.Value) + ?? throw new NullReferenceException($"unable to find writing system with id '{wsId.Code}' of type {type}"); } - if (lcmWs is not null && type is not null) + + if (wsId == default) { - var validWs = type switch - { - WritingSystemType.Analysis => wsContainer.AnalysisWritingSystems, - WritingSystemType.Vernacular => wsContainer.VernacularWritingSystems, - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) - }; - if (!validWs.Contains(lcmWs)) - { - throw new InvalidOperationException($"Writing system {ws} is not of the requested type: {type}."); - } + throw new ArgumentException("Cannot look up default writing system ID when type is not specified", nameof(wsId)); } - if (lcmWs is null) + + if (!cache.ServiceLocator.WritingSystemManager.TryGet(wsId.Code, out var lcmWs)) { - throw new NullReferenceException($"unable to find writing system with id {ws}"); + throw new NullReferenceException($"unable to find writing system with id '{wsId.Code}'"); } - return lcmWs.Handle; + return lcmWs ?? throw new NullReferenceException($"unable to find writing system with id {wsId}"); + } + + internal static int GetWritingSystemHandle(this LcmCache cache, WritingSystemId wsId, WritingSystemType? type = null) + { + var ws = GetCoreWritingSystem(cache, wsId, type); + return ws.Handle; } internal static string PickText(this ICmObject obj, ITsMultiString multiString, string ws) diff --git a/backend/FwLite/FwLiteProjectSync.Tests/WritingSystemSyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/WritingSystemSyncTests.cs index 4150a1bca0..2c246a31b9 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/WritingSystemSyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/WritingSystemSyncTests.cs @@ -50,32 +50,25 @@ public async Task DisposeAsync() [Fact] public async Task SyncWs_UpdatesOrder() { - var en = await _fixture.FwDataApi.GetWritingSystem("en", WritingSystemType.Vernacular); + var fwVernacularWSs = (await _fixture.FwDataApi.GetWritingSystems())!.Vernacular; + var crdtVernacularWSs = (await _fixture.CrdtApi.GetWritingSystems())!.Vernacular; + fwVernacularWSs.Should().HaveCount(2); + crdtVernacularWSs.Should().HaveCount(2); + fwVernacularWSs.Should().BeEquivalentTo(crdtVernacularWSs, options => options.WithStrictOrdering().Excluding(ws => ws.Order)); + var en = await _fixture.CrdtApi.GetWritingSystem("en", WritingSystemType.Vernacular); + var fr = await _fixture.CrdtApi.GetWritingSystem("fr", WritingSystemType.Vernacular); en.Should().NotBeNull(); - en.Order.Should().Be(0); // 1st - fw order starts at 0 - var fr = await _fixture.FwDataApi.GetWritingSystem("fr", WritingSystemType.Vernacular); fr.Should().NotBeNull(); - fr.Order.Should().Be(1); - var crdtEn = await _fixture.CrdtApi.GetWritingSystem("en", WritingSystemType.Vernacular); - crdtEn.Should().NotBeNull(); - crdtEn.Order.Should().Be(1); // 1st - crdt order starts at 1 - var crdtFr = await _fixture.CrdtApi.GetWritingSystem("fr", WritingSystemType.Vernacular); - crdtFr.Should().NotBeNull(); - crdtFr.Order.Should().Be(2); - + fwVernacularWSs.Should().BeEquivalentTo([en, fr], options => options.WithStrictOrdering().Excluding(ws => ws.Order)); // act - move fr before en await _fixture.FwDataApi.MoveWritingSystem("fr", WritingSystemType.Vernacular, new(null, "en")); - fr = await _fixture.FwDataApi.GetWritingSystem("fr", WritingSystemType.Vernacular); - fr.Should().NotBeNull(); - fr.Order.Should().Be(0); + var updatedFwVernacularWSs = (await _fixture.FwDataApi.GetWritingSystems())!.Vernacular; + updatedFwVernacularWSs.Should().BeEquivalentTo([fr, en], options => options.WithStrictOrdering().Excluding(ws => ws.Order)); await _syncService.Sync(_fixture.CrdtApi, _fixture.FwDataApi); // assert - var updatedCrdtEn = await _fixture.CrdtApi.GetWritingSystem("en", WritingSystemType.Vernacular); - updatedCrdtEn.Should().NotBeNull(); - var updatedCrdtFr = await _fixture.CrdtApi.GetWritingSystem("fr", WritingSystemType.Vernacular); - updatedCrdtFr.Should().NotBeNull(); - updatedCrdtFr.Order.Should().BeLessThan(updatedCrdtEn.Order); + var updatedCrdtVernacularWSs = (await _fixture.CrdtApi.GetWritingSystems())!.Vernacular; + updatedCrdtVernacularWSs.Should().BeEquivalentTo(updatedFwVernacularWSs, options => options.WithStrictOrdering().Excluding(ws => ws.Order)); } } diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 0a7ce51d75..2abc0a684e 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -1,5 +1,4 @@ using System.Data; -using Gridify; using SIL.Harmony; using SIL.Harmony.Changes; using LcmCrdt.Changes; @@ -8,7 +7,6 @@ using LcmCrdt.FullTextSearch; using LcmCrdt.MediaServer; using LcmCrdt.Objects; -using LcmCrdt.Utils; using LinqToDB; using LinqToDB.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -18,7 +16,6 @@ using SIL.Harmony.Core; using MiniLcm.Culture; using MiniLcm.Media; -using SystemTextJsonPatch; namespace LcmCrdt; @@ -68,10 +65,7 @@ private void AssertWritable() public async Task GetWritingSystems() { await using var repo = await repoFactory.CreateRepoAsync(); - var systems = await repo.WritingSystems - .OrderBy(ws => ws.Order) - .ThenBy(ws => ws.WsId) - .ToArrayAsync(); + var systems = await repo.WritingSystemsOrdered.ToArrayAsync(); return new WritingSystems { Analysis = [.. systems.Where(ws => ws.Type == WritingSystemType.Analysis)], diff --git a/backend/FwLite/LcmCrdt/Data/MiniLcmRepository.cs b/backend/FwLite/LcmCrdt/Data/MiniLcmRepository.cs index 3da29ef833..5106fc0ee1 100644 --- a/backend/FwLite/LcmCrdt/Data/MiniLcmRepository.cs +++ b/backend/FwLite/LcmCrdt/Data/MiniLcmRepository.cs @@ -14,7 +14,7 @@ namespace LcmCrdt.Data; public class MiniLcmRepositoryFactory( - Microsoft.EntityFrameworkCore.IDbContextFactory dbContextFactory, + IDbContextFactory dbContextFactory, IServiceProvider serviceProvider, EntrySearchServiceFactory? entrySearchServiceFactory = null) { @@ -67,6 +67,7 @@ public void Dispose() public IQueryable Senses => dbContext.Senses; public IQueryable ExampleSentences => dbContext.ExampleSentences; public IQueryable WritingSystems => dbContext.WritingSystems; + public IQueryable WritingSystemsOrdered => dbContext.WritingSystemsOrdered; public IQueryable SemanticDomains => dbContext.SemanticDomains; public IQueryable PartsOfSpeech => dbContext.PartsOfSpeech; public IQueryable Publications => dbContext.Publications; @@ -81,14 +82,14 @@ public void Dispose() return type switch { WritingSystemType.Analysis => _defaultAnalysisWs ??= - await AsyncExtensions.FirstOrDefaultAsync(dbContext.WritingSystems, ws => ws.Type == type), + await AsyncExtensions.FirstOrDefaultAsync(WritingSystemsOrdered, ws => ws.Type == type), WritingSystemType.Vernacular => _defaultVernacularWs ??= - await AsyncExtensions.FirstOrDefaultAsync(dbContext.WritingSystems, ws => ws.Type == type), + await AsyncExtensions.FirstOrDefaultAsync(WritingSystemsOrdered, ws => ws.Type == type), _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) } ?? throw new NullReferenceException($"Unable to find a default writing system of type {type}"); } - return await AsyncExtensions.FirstOrDefaultAsync(dbContext.WritingSystems, ws => ws.WsId == id && ws.Type == type); + return await AsyncExtensions.FirstOrDefaultAsync(WritingSystemsOrdered, ws => ws.WsId == id && ws.Type == type); } public async Task CreateComplexFormComponentChange( diff --git a/backend/FwLite/LcmCrdt/FullTextSearch/EntrySearchService.cs b/backend/FwLite/LcmCrdt/FullTextSearch/EntrySearchService.cs index 3942cbcd47..280d013f7f 100644 --- a/backend/FwLite/LcmCrdt/FullTextSearch/EntrySearchService.cs +++ b/backend/FwLite/LcmCrdt/FullTextSearch/EntrySearchService.cs @@ -1,5 +1,3 @@ -using System.Globalization; -using System.Linq.Expressions; using System.Text; using LcmCrdt.Data; using LinqToDB; @@ -139,7 +137,7 @@ public async Task UpdateEntrySearchTable(Guid entryId) public async Task UpdateEntrySearchTable(Entry entry) { - var writingSystems = await dbContext.WritingSystems.OrderBy(ws => ws.Order).ToArrayAsync(); + var writingSystems = await dbContext.WritingSystemsOrdered.ToArrayAsync(); var record = ToEntrySearchRecord(entry, writingSystems); await InsertOrUpdateEntrySearchRecord(record, EntrySearchRecordsTable); } @@ -181,7 +179,12 @@ public static async Task UpdateEntrySearchTable(IEnumerable entries, ..dbContext.WritingSystems, ..newWritingSystems ]; - Array.Sort(writingSystems, (ws1, ws2) => ws1.Order.CompareTo(ws2.Order)); + Array.Sort(writingSystems, (ws1, ws2) => + { + var orderComparison = ws1.Order.CompareTo(ws2.Order); + if (orderComparison != 0) return orderComparison; + return ws1.Id.CompareTo(ws2.Id); + }); var entrySearchRecordsTable = dbContext.GetTable(); var searchRecords = entries.Select(entry => ToEntrySearchRecord(entry, writingSystems)); foreach (var entrySearchRecord in searchRecords) @@ -200,7 +203,7 @@ public async Task RegenerateEntrySearchTable() await using var transaction = await dbContext.Database.BeginTransactionAsync(); await EntrySearchRecordsTable.TruncateAsync(); - var writingSystems = await dbContext.WritingSystems.OrderBy(ws => ws.Order).ToArrayAsync(); + var writingSystems = await dbContext.WritingSystemsOrdered.ToArrayAsync(); await EntrySearchRecordsTable .BulkCopyAsync(dbContext.Set() .LoadWith(e => e.Senses) diff --git a/backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs b/backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs index e4f3eb1e1c..50c0edd729 100644 --- a/backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs +++ b/backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using LcmCrdt.Data; using LcmCrdt.FullTextSearch; using SIL.Harmony; using SIL.Harmony.Db; @@ -17,6 +16,8 @@ IOptions options { public DbSet ProjectData => Set(); public IQueryable WritingSystems => Set().AsNoTracking(); + public IQueryable WritingSystemsOrdered => Set().AsNoTracking() + .OrderBy(ws => ws.Order).ThenBy(ws => ws.Id); public IQueryable Entries => Set().AsNoTracking(); public IQueryable ComplexFormComponents => Set().AsNoTracking(); public IQueryable ComplexFormTypes => Set().AsNoTracking(); diff --git a/backend/FwLite/MiniLcm.Tests/WritingSystemTestsBase.cs b/backend/FwLite/MiniLcm.Tests/WritingSystemTestsBase.cs index 4112ac32d0..63f814c631 100644 --- a/backend/FwLite/MiniLcm.Tests/WritingSystemTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/WritingSystemTestsBase.cs @@ -1,5 +1,7 @@ +using System.Runtime.CompilerServices; using MiniLcm.Exceptions; using MiniLcm.SyncHelpers; +using SIL.Extensions; namespace MiniLcm.Tests; @@ -13,7 +15,7 @@ public async Task GetWritingSystems_DoesNotReturnNullOrEmpty() writingSystems.Analysis.Should().NotBeNullOrEmpty(); } - [Fact] + [Fact(Skip = "Exemplars are not used, expensive and inconsistently populated (all WSs vs single WS). So we disabled populating them.")] public async Task GetWritingSystems_ReturnsExemplars() { var writingSystems = await Api.GetWritingSystems(); @@ -76,6 +78,8 @@ public async Task UpdateExistingWritingSystem_Works() [Fact] public async Task MoveWritingSystem_Works() { + var en = await Api.GetWritingSystem("en", WritingSystemType.Vernacular); + en.Should().NotBeNull(); var ws1 = await Api.CreateWritingSystem(new() { Id = Guid.NewGuid(), @@ -94,22 +98,16 @@ public async Task MoveWritingSystem_Works() Abbreviation = "Fr", Font = "Arial" }); - ws2.Order.Should().BeGreaterThan(ws1.Order); + var vernacularWSs = (await Api.GetWritingSystems())!.Vernacular; + vernacularWSs.Should().BeEquivalentTo([en, ws1, ws2], + options => options.WithStrictOrdering().Excluding(ws => ws.Order)); //act - await Api.MoveWritingSystem(ws2.WsId, WritingSystemType.Vernacular, new(null, ws1.WsId)); + await Api.MoveWritingSystem(ws2.WsId, WritingSystemType.Vernacular, new(en.WsId, ws1.WsId)); //assert - ws1 = await Api.GetWritingSystem(ws1.WsId, WritingSystemType.Vernacular); - ws1.Should().NotBeNull(); - ws2 = await Api.GetWritingSystem(ws2.WsId, WritingSystemType.Vernacular); - ws2.Should().NotBeNull(); - ws2.Order.Should().BeLessThan(ws1.Order); - - var writingSystems = await Api.GetWritingSystems(); - var en = writingSystems.Vernacular.Single(ws => ws.WsId.Code == "en"); - writingSystems.Vernacular.Should().BeEquivalentTo([en, ws2, ws1], - // we care about the order of return, not the internal Order property + vernacularWSs = (await Api.GetWritingSystems())!.Vernacular; + vernacularWSs.Should().BeEquivalentTo([en, ws2, ws1], options => options.WithStrictOrdering().Excluding(ws => ws.Order)); } @@ -145,4 +143,34 @@ public async Task InsertWritingSystem_Works() // we care about the order of return, not the internal Order property options => options.WithStrictOrdering().Excluding(ws => ws.Order)); } + + [Fact] + public async Task CanChangeDefaultWritingSystem() + { + // arrange + var currentDefault = await Api.GetWritingSystem(default, WritingSystemType.Vernacular); + var writingSystems = await Api.GetWritingSystems(); + var en = writingSystems.Vernacular.Single(ws => ws.WsId.Code == "en"); + currentDefault.Should().BeEquivalentTo(en); + writingSystems.Vernacular.First().Should().BeEquivalentTo(en); + + // act + var es = await Api.CreateWritingSystem(new() + { + Id = Guid.NewGuid(), + WsId = "es", + Type = WritingSystemType.Vernacular, + Name = "Spanish", + Abbreviation = "Es", + Font = "Arial" + }, new BetweenPosition(null, en.WsId)); + + //assert + var newDefault = await Api.GetWritingSystem(default, WritingSystemType.Vernacular); + newDefault.Should().BeEquivalentTo(es, + options => options.Excluding(ws => ws.Order)); + writingSystems = await Api.GetWritingSystems(); + writingSystems.Vernacular.First().Should().BeEquivalentTo(es, + options => options.Excluding(ws => ws.Order)); + } }