Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync complex forms #1214

Merged
merged 4 commits into from
Nov 6, 2024
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using FwDataMiniLcmBridge.Tests.Fixtures;

namespace FwDataMiniLcmBridge.Tests.MiniLcmTests;

[Collection(ProjectLoaderFixture.Name)]
public class ComplexFormComponentTests(ProjectLoaderFixture fixture): ComplexFormComponentTestsBase
{
protected override Task<IMiniLcmApi> NewApi()
{
return Task.FromResult<IMiniLcmApi>(fixture.NewProjectApi("complex-form-component-test", "en", "en"));
}
}
129 changes: 109 additions & 20 deletions backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
using System.Reflection;
using System.Text;
using FwDataMiniLcmBridge.Api.UpdateProxy;
using FwDataMiniLcmBridge.LcmUtils;
using Microsoft.Extensions.Logging;
using MiniLcm;
using MiniLcm.Models;
using MiniLcm.SyncHelpers;
using SIL.LCModel;
using SIL.LCModel.Core.KernelInterfaces;
using SIL.LCModel.Core.Text;
Expand Down Expand Up @@ -151,7 +153,7 @@ internal void CompleteExemplars(WritingSystems writingSystems)
public Task<WritingSystem> CreateWritingSystem(WritingSystemType type, WritingSystem writingSystem)
{
CoreWritingSystemDefinition? ws = null;
UndoableUnitOfWorkHelper.Do("Create Writing System",
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Writing System",
"Remove writing system",
Cache.ServiceLocator.ActionHandler,
() =>
Expand Down Expand Up @@ -201,7 +203,7 @@ public IAsyncEnumerable<PartOfSpeech> GetPartsOfSpeech()
public Task CreatePartOfSpeech(PartOfSpeech partOfSpeech)
{
if (partOfSpeech.Id == default) partOfSpeech.Id = Guid.NewGuid();
UndoableUnitOfWorkHelper.Do("Create Part of Speech",
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Part of Speech",
"Remove part of speech",
Cache.ServiceLocator.ActionHandler,
() =>
Expand Down Expand Up @@ -231,7 +233,7 @@ public IAsyncEnumerable<SemanticDomain> GetSemanticDomains()
public Task CreateSemanticDomain(SemanticDomain semanticDomain)
{
if (semanticDomain.Id == Guid.Empty) semanticDomain.Id = Guid.NewGuid();
UndoableUnitOfWorkHelper.Do("Create Semantic Domain",
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Semantic Domain",
"Remove semantic domain",
Cache.ActionHandlerAccessor,
() =>
Expand Down Expand Up @@ -266,7 +268,7 @@ private ComplexFormType ToComplexFormType(ICmPossibility t)
public Task<ComplexFormType> CreateComplexFormType(ComplexFormType complexFormType)
{
if (complexFormType.Id != default) throw new InvalidOperationException("Complex form type id must be empty");
UndoableUnitOfWorkHelper.Do("Create complex form type",
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create complex form type",
"Remove complex form type",
Cache.ActionHandlerAccessor,
() =>
Expand Down Expand Up @@ -299,7 +301,7 @@ private Entry FromLexEntry(ILexEntry entry)
LiteralMeaning = FromLcmMultiString(entry.LiteralMeaning),
Senses = entry.AllSenses.Select(FromLexSense).ToList(),
ComplexFormTypes = ToComplexFormTypes(entry),
Components = ToComplexFormComponents(entry),
Components = ToComplexFormComponents(entry).ToList(),
ComplexForms = [
..entry.ComplexFormEntries.Select(complexEntry => ToEntryReference(entry, complexEntry)),
..entry.AllSenses.SelectMany(sense => sense.ComplexFormEntries.Select(complexEntry => ToSenseReference(sense, complexEntry)))
Expand All @@ -314,7 +316,7 @@ private IList<ComplexFormType> ToComplexFormTypes(ILexEntry entry)
.Select(ToComplexFormType)
.ToList() ?? [];
}
private IList<ComplexFormComponent> ToComplexFormComponents(ILexEntry entry)
private IEnumerable<ComplexFormComponent> ToComplexFormComponents(ILexEntry entry)
{
return entry.ComplexFormEntryRefs.SingleOrDefault()
?.ComponentLexemesRS
Expand All @@ -323,8 +325,7 @@ private IList<ComplexFormComponent> ToComplexFormComponents(ILexEntry entry)
ILexEntry component => ToEntryReference(component, entry),
ILexSense s => ToSenseReference(s, entry),
_ => throw new NotSupportedException($"object type {o.ClassName} not supported")
})
.ToList() ?? [];
}) ?? [];
}

private Variants? ToVariants(ILexEntry entry)
Expand Down Expand Up @@ -481,7 +482,7 @@ public IAsyncEnumerable<Entry> SearchEntries(string query, QueryOptions? options
public async Task<Entry> CreateEntry(Entry entry)
{
entry.Id = entry.Id == default ? Guid.NewGuid() : entry.Id;
UndoableUnitOfWorkHelper.Do("Create Entry",
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Entry",
"Remove entry",
Cache.ServiceLocator.ActionHandler,
() =>
Expand Down Expand Up @@ -518,6 +519,57 @@ public async Task<Entry> CreateEntry(Entry entry)
return await GetEntry(entry.Id) ?? throw new InvalidOperationException("Entry was not created");
}

public Task<ComplexFormComponent> CreateComplexFormComponent(ComplexFormComponent complexFormComponent)
{
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Complex Form Component",
"Remove Complex Form Component",
Cache.ServiceLocator.ActionHandler,
() =>
{
var lexEntry = EntriesRepository.GetObject(complexFormComponent.ComplexFormEntryId);
AddComplexFormComponent(lexEntry, complexFormComponent);
});
return Task.FromResult(ToComplexFormComponents(EntriesRepository.GetObject(complexFormComponent.ComplexFormEntryId))
.Single(c => c.ComponentEntryId == complexFormComponent.ComponentEntryId && c.ComponentSenseId == complexFormComponent.ComponentSenseId));
}

public Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent)
{
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Delete Complex Form Component",
"Add Complex Form Component",
Cache.ServiceLocator.ActionHandler,
() =>
{
var lexEntry = EntriesRepository.GetObject(complexFormComponent.ComplexFormEntryId);
RemoveComplexFormComponent(lexEntry, complexFormComponent);
});
return Task.CompletedTask;
}

public Task AddComplexFormType(Guid entryId, Guid complexFormTypeId)
{
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Add Complex Form Type",
"Remove Complex Form Type",
Cache.ServiceLocator.ActionHandler,
() =>
{
AddComplexFormType(EntriesRepository.GetObject(entryId), complexFormTypeId);
});
return Task.CompletedTask;
}

public Task RemoveComplexFormType(Guid entryId, Guid complexFormTypeId)
{
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Remove Complex Form Type",
"Add Complex Form Type",
Cache.ServiceLocator.ActionHandler,
() =>
{
RemoveComplexFormType(EntriesRepository.GetObject(entryId), complexFormTypeId);
});
return Task.CompletedTask;
}

/// <summary>
/// must be called as part of an lcm action
/// </summary>
Expand All @@ -531,9 +583,20 @@ internal void AddComplexFormComponent(ILexEntry lexEntry, ComplexFormComponent c

internal void RemoveComplexFormComponent(ILexEntry lexEntry, ComplexFormComponent component)
{
ICmObject lexComponent = component.ComponentSenseId is not null
? SenseRepository.GetObject(component.ComponentSenseId.Value)
: EntriesRepository.GetObject(component.ComponentEntryId);
ICmObject lexComponent;
if (component.ComponentSenseId is not null)
{
//sense has been deleted, so this complex form has been deleted already
if (!SenseRepository.TryGetObject(component.ComponentSenseId.Value, out var sense)) return;
lexComponent = sense;
}
else
{
//entry has been deleted, so this complex form has been deleted already
if (!EntriesRepository.TryGetObject(component.ComponentEntryId, out var entry)) return;
lexComponent = entry;
}

var entryRef = lexEntry.ComplexFormEntryRefs.Single();
if (!entryRef.ComponentLexemesRS.Remove(lexComponent))
{
Expand Down Expand Up @@ -588,7 +651,7 @@ private void UpdateLcmMultiString(ITsMultiString multiString, MultiString newMul
public Task<Entry> UpdateEntry(Guid id, UpdateObjectInput<Entry> update)
{
var lexEntry = EntriesRepository.GetObject(id);
UndoableUnitOfWorkHelper.Do("Update Entry",
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Update Entry",
"Revert entry",
Cache.ServiceLocator.ActionHandler,
() =>
Expand All @@ -601,7 +664,7 @@ public Task<Entry> UpdateEntry(Guid id, UpdateObjectInput<Entry> update)

public Task DeleteEntry(Guid id)
{
UndoableUnitOfWorkHelper.Do("Delete Entry",
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Delete Entry",
"Revert delete",
Cache.ServiceLocator.ActionHandler,
() =>
Expand Down Expand Up @@ -653,7 +716,7 @@ public Task<Sense> CreateSense(Guid entryId, Sense sense)
if (sense.Id == default) sense.Id = Guid.NewGuid();
if (!EntriesRepository.TryGetObject(entryId, out var lexEntry))
throw new InvalidOperationException("Entry not found");
UndoableUnitOfWorkHelper.Do("Create Sense",
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Sense",
"Remove sense",
Cache.ServiceLocator.ActionHandler,
() => CreateSense(lexEntry, sense));
Expand All @@ -664,7 +727,7 @@ public Task<Sense> UpdateSense(Guid entryId, Guid senseId, UpdateObjectInput<Sen
{
var lexSense = SenseRepository.GetObject(senseId);
if (lexSense.Entry.Guid != entryId) throw new InvalidOperationException($"Sense {senseId} does not belong to the expected entry, expected Id {entryId}, actual Id {lexSense.Entry.Guid}");
UndoableUnitOfWorkHelper.Do("Update Sense",
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Update Sense",
"Revert sense",
Cache.ServiceLocator.ActionHandler,
() =>
Expand All @@ -675,11 +738,37 @@ public Task<Sense> UpdateSense(Guid entryId, Guid senseId, UpdateObjectInput<Sen
return Task.FromResult(FromLexSense(lexSense));
}

public Task AddSemanticDomainToSense(Guid senseId, SemanticDomain semanticDomain)
{
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Add Semantic Domain to Sense",
"Remove Semantic Domain from Sense",
Cache.ServiceLocator.ActionHandler,
() =>
{
var lexSense = SenseRepository.GetObject(senseId);
lexSense.SemanticDomainsRC.Add(GetLcmSemanticDomain(semanticDomain.Id));
});
return Task.CompletedTask;
}

public Task RemoveSemanticDomainFromSense(Guid senseId, Guid semanticDomainId)
{
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Remove Semantic Domain from Sense",
"Add Semantic Domain to Sense",
Cache.ServiceLocator.ActionHandler,
() =>
{
var lexSense = SenseRepository.GetObject(senseId);
lexSense.SemanticDomainsRC.Remove(lexSense.SemanticDomainsRC.First(sd => sd.Guid == semanticDomainId));
});
return Task.CompletedTask;
}

public Task DeleteSense(Guid entryId, Guid senseId)
{
var lexSense = SenseRepository.GetObject(senseId);
if (lexSense.Entry.Guid != entryId) throw new InvalidOperationException("Sense does not belong to entry");
UndoableUnitOfWorkHelper.Do("Delete Sense",
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Delete Sense",
"Revert delete",
Cache.ServiceLocator.ActionHandler,
() => lexSense.Delete());
Expand All @@ -702,7 +791,7 @@ public Task<ExampleSentence> CreateExampleSentence(Guid entryId, Guid senseId, E
if (exampleSentence.Id == default) exampleSentence.Id = Guid.NewGuid();
if (!SenseRepository.TryGetObject(senseId, out var lexSense))
throw new InvalidOperationException("Sense not found");
UndoableUnitOfWorkHelper.Do("Create Example Sentence",
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Example Sentence",
"Remove example sentence",
Cache.ServiceLocator.ActionHandler,
() => CreateExampleSentence(lexSense, exampleSentence));
Expand All @@ -716,7 +805,7 @@ public Task<ExampleSentence> UpdateExampleSentence(Guid entryId,
{
var lexExampleSentence = ExampleSentenceRepository.GetObject(exampleSentenceId);
ValidateOwnership(lexExampleSentence, entryId, senseId);
UndoableUnitOfWorkHelper.Do("Update Example Sentence",
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Update Example Sentence",
"Revert example sentence",
Cache.ServiceLocator.ActionHandler,
() =>
Expand All @@ -731,7 +820,7 @@ public Task DeleteExampleSentence(Guid entryId, Guid senseId, Guid exampleSenten
{
var lexExampleSentence = ExampleSentenceRepository.GetObject(exampleSentenceId);
ValidateOwnership(lexExampleSentence, entryId, senseId);
UndoableUnitOfWorkHelper.Do("Delete Example Sentence",
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Delete Example Sentence",
"Revert delete",
Cache.ServiceLocator.ActionHandler,
() => lexExampleSentence.Delete());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace FwDataMiniLcmBridge.Api.UpdateProxy;

public class UpdateComplexFormTypeProxy : ComplexFormType
public record UpdateComplexFormTypeProxy : ComplexFormType
{
private readonly ILexEntryType _lexEntryType;
private readonly ILexEntry _lcmEntry;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,13 @@
new UpdateListProxy<SemanticDomain>(
semanticDomain =>
{
if (semanticDomain.Id != default && lexboxLcmApi.GetLcmSemanticDomain(semanticDomain.Id) is { } lcmSemanticDomain)
sense.SemanticDomainsRC.Add(lcmSemanticDomain);
#pragma warning disable VSTHRD002
lexboxLcmApi.AddSemanticDomainToSense(sense.Guid, semanticDomain).Wait();
},
semanticDomain =>
{
if (semanticDomain.Id != default)
sense.SemanticDomainsRC.Remove(
sense.SemanticDomainsRC.First(sd => sd.Guid == semanticDomain.Id));
lexboxLcmApi.RemoveSemanticDomainFromSense(sense.Guid, semanticDomain.Id).Wait();
#pragma warning restore VSTHRD002
},
i => new UpdateProxySemanticDomain(sense.SemanticDomainsRC,
sense.SemanticDomainsRC.ElementAt(i).Guid,
Expand Down Expand Up @@ -119,7 +118,7 @@
get =>
new UpdateListProxy<ExampleSentence>(
sentence => lexboxLcmApi.CreateExampleSentence(sense, sentence),
sentence => lexboxLcmApi.DeleteExampleSentence(sense.Owner.Guid, Id, sentence.Id),

Check warning on line 121 in backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method (https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD110.md)

Check warning on line 121 in backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method (https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD110.md)

Check warning on line 121 in backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method (https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD110.md)

Check warning on line 121 in backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method (https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD110.md)

Check warning on line 121 in backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method (https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD110.md)

Check warning on line 121 in backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method (https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD110.md)

Check warning on line 121 in backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method (https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD110.md)

Check warning on line 121 in backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

Observe the awaitable result of this method call by awaiting it, assigning to a variable, or passing it to another method (https://github.com/Microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD110.md)
i => new UpdateExampleSentenceProxy(sense.ExamplesOS[i], lexboxLcmApi),
sense.ExamplesOS.Count
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using SIL.LCModel;
using SIL.LCModel.Core.KernelInterfaces;
using SIL.LCModel.Infrastructure;

namespace FwDataMiniLcmBridge.LcmUtils;

public static class ActionHandlerHelpers
{
public static async ValueTask DoUsingNewOrCurrentUOW(
this LcmCache cache,
string description,
string revertDescription,
Func<ValueTask> action)
{
var actionHandler = cache.ServiceLocator.ActionHandler;
if (actionHandler.CurrentDepth > 0)
{
await action();
return;
}

using var undoHelper = new UndoableUnitOfWorkHelper(actionHandler, description, revertDescription);
await action();
undoHelper.RollBack = false; // task ran successfully, don't roll back.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ private SyncFixture(string projectName)
_services = crdtServices.CreateAsyncScope();
}

public SyncFixture(): this("sena-3")
public SyncFixture(): this("sena-3_" + Guid.NewGuid().ToString("N"))
{
}

Expand Down
Loading
Loading