From 85f9555f3b31db53110473741a80098e5bef9d90 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Mon, 11 Nov 2024 16:07:53 +0700 Subject: [PATCH] CRDT sync handles semantic domains (#1217) * Add failing sync test for semantic domains * Implement semantic domain sync - Add GetSemanticDomain() to read API - CreateSemanticDomain now returns the created object - Add UpdateSemanticDomain() to write API - Add DeleteSemanticDomain() to write API - Add UpdateSemanticDomainProxy class - Add SemanticDomainSync class - Sync semantic domains in CRDT-FW sync service * Get sync test passing * Add new test for semdoms syncing in entries * Fix Predefined inconsistency for semantic domains --------- Co-authored-by: Kevin Hahn --- backend/FwHeadless/FwHeadless.csproj | 2 + .../Api/FwDataMiniLcmApi.cs | 62 +++++++++++---- .../UpdateProxy/UpdateSemanticDomainProxy.cs | 30 +++++++ .../FwLiteProjectSync.Tests/SyncTests.cs | 79 +++++++++++++++++++ .../CrdtFwdataProjectSyncService.cs | 13 ++- .../FwLiteProjectSync/DryRunMiniLcmApi.cs | 22 +++++- backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs | 26 +++++- .../Objects/JsonPatchChangeExtractor.cs | 8 +- backend/FwLite/MiniLcm/IMiniLcmReadApi.cs | 3 +- backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 12 ++- .../FwLite/MiniLcm/Models/SemanticDomain.cs | 8 +- .../MiniLcm/SyncHelpers/SemanticDomainSync.cs | 47 +++++++++++ backend/LfClassicData/LfClassicMiniLcmApi.cs | 54 ++++++++++--- 13 files changed, 325 insertions(+), 41 deletions(-) create mode 100644 backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSemanticDomainProxy.cs create mode 100644 backend/FwLite/MiniLcm/SyncHelpers/SemanticDomainSync.cs diff --git a/backend/FwHeadless/FwHeadless.csproj b/backend/FwHeadless/FwHeadless.csproj index 335b1e00b..82dcf0521 100644 --- a/backend/FwHeadless/FwHeadless.csproj +++ b/backend/FwHeadless/FwHeadless.csproj @@ -24,6 +24,8 @@ + + diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 7c400993c..50f2a2aa6 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -1,4 +1,4 @@ -using System.Collections.Frozen; +using System.Collections.Frozen; using System.Reflection; using System.Text; using FwDataMiniLcmBridge.Api.UpdateProxy; @@ -246,6 +246,17 @@ public Task DeletePartOfSpeech(Guid id) return Task.CompletedTask; } + internal SemanticDomain FromLcmSemanticDomain(ICmSemanticDomain semanticDomain) + { + return new SemanticDomain + { + Id = semanticDomain.Guid, + Name = FromLcmMultiString(semanticDomain.Name), + Code = semanticDomain.Abbreviation.UiString ?? "", + Predefined = true, // TODO: Look up in a GUID list of predefined data + }; + } + public IAsyncEnumerable GetSemanticDomains() { return @@ -253,15 +264,16 @@ public IAsyncEnumerable GetSemanticDomains() .AllInstances() .OrderBy(p => p.Abbreviation.UiString) .ToAsyncEnumerable() - .Select(semanticDomain => new SemanticDomain - { - Id = semanticDomain.Guid, - Name = FromLcmMultiString(semanticDomain.Name), - Code = semanticDomain.Abbreviation.UiString ?? "" - }); + .Select(FromLcmSemanticDomain); } - public Task CreateSemanticDomain(SemanticDomain semanticDomain) + public Task GetSemanticDomain(Guid id) + { + var semDom = GetLcmSemanticDomain(id); + return Task.FromResult(semDom is null ? null : FromLcmSemanticDomain(semDom)); + } + + public async Task CreateSemanticDomain(SemanticDomain semanticDomain) { if (semanticDomain.Id == Guid.Empty) semanticDomain.Id = Guid.NewGuid(); UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Semantic Domain", @@ -273,8 +285,35 @@ public Task CreateSemanticDomain(SemanticDomain semanticDomain) .Create(semanticDomain.Id, Cache.LangProject.SemanticDomainListOA); lcmSemanticDomain.OcmCodes = semanticDomain.Code; UpdateLcmMultiString(lcmSemanticDomain.Name, semanticDomain.Name); + // TODO: Find out if semantic domains are guaranteed to have an "en" writing system, or if we should use lcmCache.DefautlAnalWs instead UpdateLcmMultiString(lcmSemanticDomain.Abbreviation, new MultiString(){{"en", semanticDomain.Code}}); }); + return await GetSemanticDomain(semanticDomain.Id) ?? throw new InvalidOperationException("Semantic domain was not created"); + } + + public Task UpdateSemanticDomain(Guid id, UpdateObjectInput update) + { + var lcmSemanticDomain = SemanticDomainRepository.GetObject(id); + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Update Semantic Domain", + "Revert Semantic Domain", + Cache.ServiceLocator.ActionHandler, + () => + { + var updateProxy = new UpdateSemanticDomainProxy(lcmSemanticDomain, this); + update.Apply(updateProxy); + }); + return Task.FromResult(FromLcmSemanticDomain(lcmSemanticDomain)); + } + + public Task DeleteSemanticDomain(Guid id) + { + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Delete Semantic Domain", + "Revert delete", + Cache.ServiceLocator.ActionHandler, + () => + { + SemanticDomainRepository.GetObject(id).Delete(); + }); return Task.CompletedTask; } @@ -428,12 +467,7 @@ private Sense FromLexSense(ILexSense sense) Definition = FromLcmMultiString(sense.Definition), PartOfSpeech = sense.MorphoSyntaxAnalysisRA?.GetPartOfSpeech()?.Name.get_String(enWs).Text ?? "", PartOfSpeechId = sense.MorphoSyntaxAnalysisRA?.GetPartOfSpeech()?.Guid, - SemanticDomains = sense.SemanticDomainsRC.Select(s => new SemanticDomain - { - Id = s.Guid, - Name = FromLcmMultiString(s.Name), - Code = s.OcmCodes - }).ToList(), + SemanticDomains = sense.SemanticDomainsRC.Select(FromLcmSemanticDomain).ToList(), ExampleSentences = sense.ExamplesOS.Select(sentence => FromLexExampleSentence(sense.Guid, sentence)).ToList() }; return s; diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSemanticDomainProxy.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSemanticDomainProxy.cs new file mode 100644 index 000000000..a154c2951 --- /dev/null +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSemanticDomainProxy.cs @@ -0,0 +1,30 @@ +using System.Diagnostics.CodeAnalysis; +using MiniLcm.Models; +using SIL.LCModel; + +namespace FwDataMiniLcmBridge.Api.UpdateProxy; + +public class UpdateSemanticDomainProxy : SemanticDomain +{ + private readonly ICmSemanticDomain _lcmSemanticDomain; + private readonly FwDataMiniLcmApi _lexboxLcmApi; + + public UpdateSemanticDomainProxy(ICmSemanticDomain lcmSemanticDomain, FwDataMiniLcmApi lexboxLcmApi) + { + _lcmSemanticDomain = lcmSemanticDomain; + Id = lcmSemanticDomain.Guid; + _lexboxLcmApi = lexboxLcmApi; + } + + public override MultiString Name + { + get => new UpdateMultiStringProxy(_lcmSemanticDomain.Name, _lexboxLcmApi); + set => throw new NotImplementedException(); + } + + public override string Code + { + get => _lcmSemanticDomain.Abbreviation.BestAnalysisVernacularAlternative.Text; + set => throw new NotImplementedException(); + } +} diff --git a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs index 7c150891b..138390e28 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs @@ -213,6 +213,85 @@ await crdtApi.CreateEntry(new Entry() .For(e => e.ComplexForms).Exclude(c => c.Id)); } + [Fact] + public async Task SemanticDomainsSyncBothWays() + { + var crdtApi = _fixture.CrdtApi; + var fwdataApi = _fixture.FwDataApi; + await _syncService.Sync(crdtApi, fwdataApi); + + var semdom3 = new SemanticDomain() + { + Id = new Guid("f4491f9b-3c5e-42ab-afc0-f22e19d0fff5"), + Name = new MultiString() { { "en", "Language and thought" } }, + Code = "3", + Predefined = true, + }; + await fwdataApi.CreateSemanticDomain(semdom3); + + var semdom4 = new SemanticDomain() + { + Id = new Guid("62b4ae33-f3c2-447a-9ef7-7e41805b6a02"), + Name = new MultiString() { { "en", "Social behavior" } }, + Code = "4", + Predefined = true, + }; + await crdtApi.CreateSemanticDomain(semdom4); + + await _syncService.Sync(crdtApi, fwdataApi); + + var crdtSemanticDomains = await crdtApi.GetSemanticDomains().ToArrayAsync(); + var fwdataSemanticDomains = await fwdataApi.GetSemanticDomains().ToArrayAsync(); + crdtSemanticDomains.Should().ContainEquivalentOf(semdom3); + crdtSemanticDomains.Should().ContainEquivalentOf(semdom4); + fwdataSemanticDomains.Should().ContainEquivalentOf(semdom3); + fwdataSemanticDomains.Should().ContainEquivalentOf(semdom4); + + crdtSemanticDomains.Should().BeEquivalentTo(fwdataSemanticDomains); + } + + [Fact] + public async Task SemanticDomainsSyncInEntries() + { + var crdtApi = _fixture.CrdtApi; + var fwdataApi = _fixture.FwDataApi; + await _syncService.Sync(crdtApi, fwdataApi); + + var semdom3 = new SemanticDomain() + { + Id = new Guid("f4491f9b-3c5e-42ab-afc0-f22e19d0fff5"), + Name = new MultiString() { { "en", "Language and thought" } }, + Code = "3", + Predefined = true, + }; + await fwdataApi.CreateSemanticDomain(semdom3); + // Note we do *not* call crdtApi.CreateSemanticDomain(semdom3); + + await fwdataApi.CreateEntry(new Entry() + { + LexemeForm = { { "en", "Pear" } }, + Senses = + [ + new Sense() { Gloss = { { "en", "Pear" } }, SemanticDomains = [ semdom3 ] } + ] + }); + await crdtApi.CreateEntry(new Entry() + { + LexemeForm = { { "en", "Banana" } }, + Senses = + [ + new Sense() { Gloss = { { "en", "Banana" } }, SemanticDomains = [ semdom3 ] } + ] + }); + await _syncService.Sync(crdtApi, fwdataApi); + + var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); + var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); + crdtEntries.Should().BeEquivalentTo(fwdataEntries, + options => options.For(e => e.Components).Exclude(c => c.Id) + .For(e => e.ComplexForms).Exclude(c => c.Id)); + } + [Fact] public async Task UpdatingAnEntryInEachProjectSyncsAcrossBoth() { diff --git a/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs b/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs index 167c58fe7..12258dcee 100644 --- a/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs +++ b/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs @@ -28,7 +28,10 @@ public async Task Sync(IMiniLcmApi crdtApi, FwDataMiniLcmApi fwdataA if (!dryRun) { await SaveProjectSnapshot(fwdataApi.Project.Name, fwdataApi.Project.ProjectsPath, - new ProjectSnapshot(await fwdataApi.GetEntries().ToArrayAsync(), await fwdataApi.GetPartsOfSpeech().ToArrayAsync())); + new ProjectSnapshot( + await fwdataApi.GetEntries().ToArrayAsync(), + await fwdataApi.GetPartsOfSpeech().ToArrayAsync(), + await fwdataApi.GetSemanticDomains().ToArrayAsync())); } return result; } @@ -48,12 +51,16 @@ private async Task Sync(IMiniLcmApi crdtApi, IMiniLcmApi fwdataApi, return new SyncResult(entryCount, 0); } - //todo sync complex form types, parts of speech, semantic domains, writing systems + //todo sync complex form types, writing systems var currentFwDataPartsOfSpeech = await fwdataApi.GetPartsOfSpeech().ToArrayAsync(); var crdtChanges = await PartOfSpeechSync.Sync(currentFwDataPartsOfSpeech, projectSnapshot.PartsOfSpeech, crdtApi); var fwdataChanges = await PartOfSpeechSync.Sync(await crdtApi.GetPartsOfSpeech().ToArrayAsync(), currentFwDataPartsOfSpeech, fwdataApi); + var currentFwDataSemanticDomains = await fwdataApi.GetSemanticDomains().ToArrayAsync(); + crdtChanges += await SemanticDomainSync.Sync(currentFwDataSemanticDomains, projectSnapshot.SemanticDomains, crdtApi); + fwdataChanges += await SemanticDomainSync.Sync(await crdtApi.GetSemanticDomains().ToArrayAsync(), currentFwDataSemanticDomains, fwdataApi); + var currentFwDataEntries = await fwdataApi.GetEntries().ToArrayAsync(); crdtChanges += await EntrySync.Sync(currentFwDataEntries, projectSnapshot.Entries, crdtApi); LogDryRun(crdtApi, "crdt"); @@ -77,7 +84,7 @@ private void LogDryRun(IMiniLcmApi api, string type) logger.LogInformation($"Dry run {type} changes: {dryRunApi.DryRunRecords.Count}"); } - public record ProjectSnapshot(Entry[] Entries, PartOfSpeech[] PartsOfSpeech); + public record ProjectSnapshot(Entry[] Entries, PartOfSpeech[] PartsOfSpeech, SemanticDomain[] SemanticDomains); private async Task GetProjectSnapshot(string projectName, string? projectPath) { diff --git a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs index d3b18caf8..959a22113 100644 --- a/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs +++ b/backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs @@ -1,4 +1,4 @@ -using MiniLcm; +using MiniLcm; using MiniLcm.Models; namespace FwLiteProjectSync; @@ -49,6 +49,7 @@ public Task CreatePartOfSpeech(PartOfSpeech partOfSpeech) DryRunRecords.Add(new DryRunRecord(nameof(CreatePartOfSpeech), $"Create part of speech {partOfSpeech.Name}")); return Task.FromResult(partOfSpeech); // Since this is a dry run, api.GetPartOfSpeech would return null } + public Task UpdatePartOfSpeech(Guid id, UpdateObjectInput update) { DryRunRecords.Add(new DryRunRecord(nameof(UpdatePartOfSpeech), $"Update part of speech {id}")); @@ -66,10 +67,27 @@ public IAsyncEnumerable GetSemanticDomains() return api.GetSemanticDomains(); } - public Task CreateSemanticDomain(SemanticDomain semanticDomain) + public Task GetSemanticDomain(Guid id) + { + return api.GetSemanticDomain(id); + } + + public Task CreateSemanticDomain(SemanticDomain semanticDomain) { DryRunRecords.Add(new DryRunRecord(nameof(CreateSemanticDomain), $"Create semantic domain {semanticDomain.Name}")); + return Task.FromResult(semanticDomain); + } + + public Task UpdateSemanticDomain(Guid id, UpdateObjectInput update) + { + DryRunRecords.Add(new DryRunRecord(nameof(UpdateSemanticDomain), $"Update part of speech {id}")); + return GetSemanticDomain(id)!; + } + + public Task DeleteSemanticDomain(Guid id) + { + DryRunRecords.Add(new DryRunRecord(nameof(DeleteSemanticDomain), $"Delete part of speech {id}")); return Task.CompletedTask; } diff --git a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs index 22ebcd6c8..83261e1d7 100644 --- a/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs +++ b/backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs @@ -1,4 +1,4 @@ -using System.Linq.Expressions; +using System.Linq.Expressions; using SIL.Harmony; using SIL.Harmony.Changes; using LcmCrdt.Changes; @@ -107,9 +107,29 @@ public async Task DeletePartOfSpeech(Guid id) return SemanticDomains.AsAsyncEnumerable(); } - public async Task CreateSemanticDomain(MiniLcm.Models.SemanticDomain semanticDomain) + public Task GetSemanticDomain(Guid id) { - await dataModel.AddChange(ClientId, new CreateSemanticDomainChange(semanticDomain.Id, semanticDomain.Name, semanticDomain.Code)); + return SemanticDomains.FirstOrDefaultAsync(semdom => semdom.Id == id); + } + + public async Task CreateSemanticDomain(MiniLcm.Models.SemanticDomain semanticDomain) + { + await dataModel.AddChange(ClientId, new CreateSemanticDomainChange(semanticDomain.Id, semanticDomain.Name, semanticDomain.Code, semanticDomain.Predefined)); + return await GetSemanticDomain(semanticDomain.Id) ?? throw new NullReferenceException(); + } + + public async Task UpdateSemanticDomain(Guid id, UpdateObjectInput update) + { + var semDom = await GetSemanticDomain(id); + if (semDom is null) throw new NullReferenceException($"unable to find semantic domain with id {id}"); + + await dataModel.AddChanges(ClientId, [..semDom.ToChanges(update.Patch)]); + return await GetSemanticDomain(id) ?? throw new NullReferenceException(); + } + + public async Task DeleteSemanticDomain(Guid id) + { + await dataModel.AddChange(ClientId, new DeleteChange(id)); } public async Task BulkImportSemanticDomains(IEnumerable semanticDomains) diff --git a/backend/FwLite/LcmCrdt/Objects/JsonPatchChangeExtractor.cs b/backend/FwLite/LcmCrdt/Objects/JsonPatchChangeExtractor.cs index 08757b4b6..9a9c4c409 100644 --- a/backend/FwLite/LcmCrdt/Objects/JsonPatchChangeExtractor.cs +++ b/backend/FwLite/LcmCrdt/Objects/JsonPatchChangeExtractor.cs @@ -1,4 +1,4 @@ -using LcmCrdt.Changes; +using LcmCrdt.Changes; using LcmCrdt.Changes.Entries; using LcmCrdt.Utils; using SIL.Harmony.Changes; @@ -154,4 +154,10 @@ public static IEnumerable ToChanges(this PartOfSpeech pos, JsonPatchDoc if (patch.Operations.Count > 0) yield return new JsonPatchChange(pos.Id, patch); } + + public static IEnumerable ToChanges(this SemanticDomain semDom, JsonPatchDocument patch) + { + if (patch.Operations.Count > 0) + yield return new JsonPatchChange(semDom.Id, patch); + } } diff --git a/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs b/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs index 9769f3705..de0bc543e 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmReadApi.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; using MiniLcm.Models; namespace MiniLcm; @@ -13,6 +13,7 @@ public interface IMiniLcmReadApi IAsyncEnumerable SearchEntries(string query, QueryOptions? options = null); Task GetEntry(Guid id); Task GetPartOfSpeech(Guid id); + Task GetSemanticDomain(Guid id); } public record QueryOptions( diff --git a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs index db45680da..e925f8cd0 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs @@ -1,4 +1,4 @@ -using System.Linq.Expressions; +using System.Linq.Expressions; using MiniLcm.Models; using SystemTextJsonPatch; @@ -13,10 +13,18 @@ Task UpdateWritingSystem(WritingSystemId id, UpdateObjectInput update); + #region PartOfSpeech Task CreatePartOfSpeech(PartOfSpeech partOfSpeech); Task UpdatePartOfSpeech(Guid id, UpdateObjectInput update); Task DeletePartOfSpeech(Guid id); - Task CreateSemanticDomain(SemanticDomain semanticDomain); + #endregion + + #region SemanticDomain + Task CreateSemanticDomain(SemanticDomain semanticDomain); + Task UpdateSemanticDomain(Guid id, UpdateObjectInput update); + Task DeleteSemanticDomain(Guid id); + #endregion + Task CreateComplexFormType(ComplexFormType complexFormType); #region Entry diff --git a/backend/FwLite/MiniLcm/Models/SemanticDomain.cs b/backend/FwLite/MiniLcm/Models/SemanticDomain.cs index 616af0adb..7d144831b 100644 --- a/backend/FwLite/MiniLcm/Models/SemanticDomain.cs +++ b/backend/FwLite/MiniLcm/Models/SemanticDomain.cs @@ -1,10 +1,10 @@ -namespace MiniLcm.Models; +namespace MiniLcm.Models; public class SemanticDomain : IObjectWithId { - public virtual required Guid Id { get; set; } - public virtual required MultiString Name { get; set; } - public virtual required string Code { get; set; } + public virtual Guid Id { get; set; } + public virtual MultiString Name { get; set; } = new(); + public virtual string Code { get; set; } = string.Empty; public DateTimeOffset? DeletedAt { get; set; } public bool Predefined { get; set; } diff --git a/backend/FwLite/MiniLcm/SyncHelpers/SemanticDomainSync.cs b/backend/FwLite/MiniLcm/SyncHelpers/SemanticDomainSync.cs new file mode 100644 index 000000000..1219db9aa --- /dev/null +++ b/backend/FwLite/MiniLcm/SyncHelpers/SemanticDomainSync.cs @@ -0,0 +1,47 @@ +using MiniLcm; +using MiniLcm.Models; +using MiniLcm.SyncHelpers; +using SystemTextJsonPatch; + +public static class SemanticDomainSync +{ + public static async Task Sync(SemanticDomain[] currentSemanticDomains, + SemanticDomain[] previousSemanticDomains, + IMiniLcmApi api) + { + return await DiffCollection.Diff(api, + previousSemanticDomains, + currentSemanticDomains, + pos => pos.Id, + async (api, currentPos) => + { + await api.CreateSemanticDomain(currentPos); + return 1; + }, + async (api, previousPos) => + { + await api.DeleteSemanticDomain(previousPos.Id); + return 1; + }, + async (api, previousPos, currentPos) => + { + var updateObjectInput = SemanticDomainDiffToUpdate(previousPos, currentPos); + if (updateObjectInput is not null) await api.UpdateSemanticDomain(currentPos.Id, updateObjectInput); + return updateObjectInput is null ? 0 : 1; + }); + } + + public static UpdateObjectInput? SemanticDomainDiffToUpdate(SemanticDomain previousSemanticDomain, SemanticDomain currentSemanticDomain) + { + JsonPatchDocument patchDocument = new(); + patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff(nameof(SemanticDomain.Name), + previousSemanticDomain.Name, + currentSemanticDomain.Name)); + // TODO: Once we add abbreviations to MiniLcm's SemanticDomain objects, then: + // patchDocument.Operations.AddRange(GetMultiStringDiff(nameof(SemanticDomain.Abbreviation), + // previousSemanticDomain.Abbreviation, + // currentSemanticDomain.Abbreviation)); + if (patchDocument.Operations.Count == 0) return null; + return new UpdateObjectInput(patchDocument); + } +} diff --git a/backend/LfClassicData/LfClassicMiniLcmApi.cs b/backend/LfClassicData/LfClassicMiniLcmApi.cs index 2bc264260..e762a5440 100644 --- a/backend/LfClassicData/LfClassicMiniLcmApi.cs +++ b/backend/LfClassicData/LfClassicMiniLcmApi.cs @@ -78,15 +78,7 @@ public async IAsyncEnumerable GetPartsOfSpeech() foreach (var item in optionListItems) { - yield return new PartOfSpeech - { - Id = item.Guid ?? Guid.Empty, - Name = new MultiString - { - { "en", item.Value ?? item.Abbreviation ?? string.Empty }, - { "__key", item.Key ?? string.Empty } // The key is all that senses have on them, so we need it client-side to find the display name - } - }; + yield return ToPartOfSpeech(item); } } @@ -95,9 +87,19 @@ public async IAsyncEnumerable GetPartsOfSpeech() return await GetPartsOfSpeech().FirstOrDefaultAsync(pos => pos.Id == id); } - public IAsyncEnumerable GetSemanticDomains() + public async IAsyncEnumerable GetSemanticDomains() { - return AsyncEnumerable.Empty(); + var optionListItems = await dbContext.GetOptionListItems(projectCode, "semantic-domain-ddp4"); + + foreach (var item in optionListItems) + { + yield return ToSemanticDomain(item); + } + } + + public async Task GetSemanticDomain(Guid id) + { + return await GetSemanticDomains().FirstOrDefaultAsync(semdom => semdom.Id == id); } public IAsyncEnumerable GetEntries(QueryOptions? options = null) @@ -276,6 +278,36 @@ private static MultiString ToMultiString(Dictionary? multiText return ms; } + private static PartOfSpeech ToPartOfSpeech(Entities.OptionListItem item) + { + return new PartOfSpeech + { + Id = item.Guid ?? Guid.Empty, + Name = new MultiString + { + { "en", item.Value ?? item.Abbreviation ?? string.Empty }, + { "__key", item.Key ?? string.Empty } // The key is all that senses have on them, so we need it client-side to find the display name + }, + // TODO: Abbreviation + Predefined = false, + }; + } + + private static SemanticDomain ToSemanticDomain(Entities.OptionListItem item) + { + // TODO: Needs testing against actual LF testlangproj data + return new SemanticDomain + { + Id = item.Guid ?? Guid.Empty, + Name = new MultiString + { + { "en", item.Value ?? item.Abbreviation ?? string.Empty }, + { "__key", item.Key ?? string.Empty } // The key is all that senses have on them, so we need it client-side to find the display name + }, + Predefined = false, + }; + } + public async Task GetEntry(Guid id) { var entry = await Entries.Find(e => e.Guid == id).FirstOrDefaultAsync();