Skip to content
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
34 changes: 34 additions & 0 deletions backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,40 @@ public async Task SyncComplexFormsAndComponents_ThrowsExceptionIfEntryNotInAfter
await act.Should().ThrowAsync<InvalidOperationException>();
}

[Fact]
public async Task CanSyncComponentWhenSenseMovesToDifferentEntry()
{
// Reproduces a real sync scenario: the snapshot (before) has a component pointing to
// sense S on entry A. In current FwData (after), sense S has moved to entry B (I don't know when that actually happens).
// So: same ComponentSenseId, different ComponentEntryId.
var senseId = Guid.NewGuid();
var oldComponentEntry = await Api.CreateEntry(new() { LexemeForm = { { "en", "old-component" } } });
var newComponentEntry = await Api.CreateEntry(new() { LexemeForm = { { "en", "new-component" } }, Senses = [new() { Id = senseId }] });
var complexFormId = Guid.NewGuid();
// Create the complex form with the old component (pointing to sense on old entry)
var complexForm = await Api.CreateEntry(new()
{
Id = complexFormId,
LexemeForm = { { "en", "complex form" } },
Components = [ComplexFormComponent.FromEntries(new Entry { Id = complexFormId }, oldComponentEntry, senseId)]
});

// after: FwData now has the component pointing to new entry with the same sense
var complexFormAfter = complexForm.Copy();
complexFormAfter.Components =
[
ComplexFormComponent.FromEntries(new Entry { Id = complexFormId }, newComponentEntry, senseId)
];

await EntrySync.SyncComplexFormsAndComponents(complexForm, complexFormAfter, Api);

var actual = await Api.GetEntry(complexFormId);
actual.Should().NotBeNull();
actual.Components.Should().HaveCount(1);
actual.Components[0].ComponentEntryId.Should().Be(newComponentEntry.Id);
actual.Components[0].ComponentSenseId.Should().Be(senseId);
}

[Fact]
public async Task SyncComplexFormsAndComponents_MovesComponentsToCorrectPosition()
{
Expand Down
13 changes: 4 additions & 9 deletions backend/FwLite/MiniLcm.Tests/DiffCollectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,22 +169,17 @@ public async Task DiffTests(CollectionDiffTestCase testCase)
return (changeCount, diffApi.DiffOperations, diffApi.Replacements);
}

private static BetweenPosition Between(TestOrderable? previous, TestOrderable? next)
private static BetweenPosition<TestOrderable> Between(TestOrderable? previous, TestOrderable? next)
{
return Between(previous?.Id, next?.Id);
return new BetweenPosition<TestOrderable>(previous, next);
}

private static BetweenPosition Between(Guid? previous = null, Guid? next = null)
{
return new BetweenPosition(previous, next);
}

private static CollectionDiffOperation Move(TestOrderable value, BetweenPosition between)
private static CollectionDiffOperation Move(TestOrderable value, BetweenPosition<TestOrderable> between)
{
return new CollectionDiffOperation(value, PositionDiffKind.Move, between);
}

private static CollectionDiffOperation Add(TestOrderable value, BetweenPosition between)
private static CollectionDiffOperation Add(TestOrderable value, BetweenPosition<TestOrderable> between)
{
return new CollectionDiffOperation(value, PositionDiffKind.Add, between);
}
Expand Down
23 changes: 14 additions & 9 deletions backend/FwLite/MiniLcm.Tests/TestOrderableDiffApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public async Task Add()

// act
var diffApi = new TestOrderableDiffApi([value1]);
var position = new BetweenPosition(value1.Id, null);
var position = new BetweenPosition<TestOrderable>(value1, null);
await diffApi.Add(value2, position);

// assert
Expand Down Expand Up @@ -53,7 +53,7 @@ public async Task Move()

// act
var diffApi = new TestOrderableDiffApi([value1, value2, value3]);
var position = new BetweenPosition(value1.Id, value2.Id);
var position = new BetweenPosition<TestOrderable>(value1, value2);
await diffApi.Move(value3, position);

// assert
Expand Down Expand Up @@ -82,28 +82,33 @@ public async Task Replace()
}
}

public class TestOrderableDiffApi(TestOrderable[] before) : IOrderableCollectionDiffApi<TestOrderable>
public class TestOrderableDiffApi(TestOrderable[] before) : IOrderableCollectionDiffApi<TestOrderable, Guid>
{
public List<TestOrderable> Current { get; } = [.. before];
public List<CollectionDiffOperation> DiffOperations = [];
public List<(TestOrderable before, TestOrderable after)> Replacements = [];

public Task<int> Add(TestOrderable value, BetweenPosition between)
public Guid GetId(TestOrderable value)
{
return value.Id;
}

public Task<int> Add(TestOrderable value, BetweenPosition<TestOrderable> between)
{
DiffOperations.Add(new CollectionDiffOperation(value, PositionDiffKind.Add, between));
return AddInternal(value, between);
}

private Task<int> AddInternal(TestOrderable value, BetweenPosition between)
private Task<int> AddInternal(TestOrderable value, BetweenPosition<TestOrderable> between)
{
if (between.Previous is not null)
{
var previousIndex = Current.FindIndex(v => v.Id == between.Previous);
var previousIndex = Current.FindIndex(v => v.Id == between.Previous.Id);
Current.Insert(previousIndex + 1, value);
}
else if (between.Next is not null)
{
var nextIndex = Current.FindIndex(v => v.Id == between.Next);
var nextIndex = Current.FindIndex(v => v.Id == between.Next.Id);
Current.Insert(nextIndex, value);
}
else
Expand All @@ -126,7 +131,7 @@ public Task<int> RemoveInternal(TestOrderable value)
return Task.FromResult(1);
}

public async Task<int> Move(TestOrderable value, BetweenPosition between)
public async Task<int> Move(TestOrderable value, BetweenPosition<TestOrderable> between)
{
DiffOperations.Add(new CollectionDiffOperation(value, PositionDiffKind.Move, between));
await RemoveInternal(value);
Expand All @@ -151,7 +156,7 @@ public Task<int> Replace(TestOrderable before, TestOrderable after)
}
}

public record CollectionDiffOperation(TestOrderable Value, PositionDiffKind Kind, BetweenPosition? Between = null);
public record CollectionDiffOperation(TestOrderable Value, PositionDiffKind Kind, BetweenPosition<TestOrderable>? Between = null);

public class TestOrderable(double order, Guid id) : IOrderable
{
Expand Down
56 changes: 32 additions & 24 deletions backend/FwLite/MiniLcm/SyncHelpers/DiffCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,13 @@ public override async Task<int> Replace(T before, T after)
}
}

public interface IOrderableCollectionDiffApi<T> where T : IOrderable
public interface IOrderableCollectionDiffApi<T, TId> where T : IOrderableNoId where TId : notnull
{
Task<int> Add(T value, BetweenPosition between);
Task<int> Add(T value, BetweenPosition<T> between);
Task<int> Remove(T value);
Task<int> Move(T value, BetweenPosition between);
Task<int> Move(T value, BetweenPosition<T> between);
Task<int> Replace(T before, T after);

Guid GetId(T value)
{
return value.Id;
}
TId GetId(T value);
}

public static class DiffCollection
Expand Down Expand Up @@ -106,10 +102,10 @@ public static async Task<int> Diff<T, TId>(
return changes;
}

public static async Task<int> DiffOrderable<T>(
public static async Task<int> DiffOrderable<T, TId>(
IList<T> before,
IList<T> after,
IOrderableCollectionDiffApi<T> diffApi) where T : IOrderable
IOrderableCollectionDiffApi<T, TId> diffApi) where T : IOrderableNoId where TId : notnull
{
var changes = 0;

Expand Down Expand Up @@ -159,38 +155,50 @@ public static async Task<int> DiffOrderable<T>(
return changes;
}

private static BetweenPosition GetStableBetween<T>(int i, IList<T> current, HashSet<Guid> stable, Func<T, Guid> GetId)
private static BetweenPosition<T> GetStableBetween<T, TId>(int i, IList<T> current, HashSet<TId> stable, Func<T, TId> getId) where TId : notnull
{
T? beforeEntity = default;
T? afterEntity = default;
T? previous = default;
T? next = default;
for (var j = i - 1; j >= 0; j--)
{
if (stable.Contains(GetId(current[j])))
if (stable.Contains(getId(current[j])))
{
beforeEntity = current[j];
previous = current[j];
break;
}
}
for (var j = i + 1; j < current.Count; j++)
{
if (stable.Contains(GetId(current[j])))
if (stable.Contains(getId(current[j])))
{
afterEntity = current[j];
next = current[j];
break;
}
}
return new BetweenPosition(
beforeEntity is not null ? GetId(beforeEntity) : null,
afterEntity is not null ? GetId(afterEntity) : null);
return new BetweenPosition<T>(previous, next);
}

private static ImmutableSortedDictionary<int, PositionDiff>? DiffPositions<T>(
private static ImmutableSortedDictionary<int, PositionDiff>? DiffPositions<T, TId>(
IList<T> before,
IList<T> after,
Func<T, Guid> GetId)
Func<T, TId> getId) where TId : notnull
{
var beforeJson = new JsonArray([.. before.Select(item => JsonValue.Create(GetId(item)))]);
var afterJson = new JsonArray([.. after.Select(item => JsonValue.Create(GetId(item)))]);
// Map TIds to integers for JSON serialization (supports non-Guid ID types like tuples)
var idToInt = new Dictionary<TId, int>();
int idx = 0;
foreach (var item in before)
{
var id = getId(item);
if (!idToInt.ContainsKey(id)) idToInt[id] = idx++;
}
foreach (var item in after)
{
var id = getId(item);
if (!idToInt.ContainsKey(id)) idToInt[id] = idx++;
}

var beforeJson = new JsonArray([.. before.Select(item => JsonValue.Create(idToInt[getId(item)]))]);
var afterJson = new JsonArray([.. after.Select(item => JsonValue.Create(idToInt[getId(item)]))]);
return JsonDiffPatcher.Diff(beforeJson, afterJson, DiffFormatter.Instance);
}

Expand Down
44 changes: 20 additions & 24 deletions backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public static async Task<int> SyncComplexFormsAndComponents(Entry beforeEntry, E
try
{
var changes = 0;
changes += await SyncComplexFormComponents(afterEntry, beforeEntry.Components, afterEntry.Components, api);
changes += await SyncComplexFormComponents(beforeEntry.Components, afterEntry.Components, api);
changes += await SyncComplexForms(beforeEntry.ComplexForms, afterEntry.ComplexForms, api);
return changes;
}
Expand Down Expand Up @@ -98,12 +98,12 @@ private static async Task<int> Sync(Guid entryId,
new ComplexFormTypesDiffApi(api, entryId));
}

private static async Task<int> SyncComplexFormComponents(Entry afterEntry, IList<ComplexFormComponent> beforeComponents, IList<ComplexFormComponent> afterComponents, IMiniLcmApi api)
private static async Task<int> SyncComplexFormComponents(IList<ComplexFormComponent> beforeComponents, IList<ComplexFormComponent> afterComponents, IMiniLcmApi api)
{
return await DiffCollection.DiffOrderable(
beforeComponents,
afterComponents,
new ComplexFormComponentsDiffApi(afterEntry, api)
new ComplexFormComponentsDiffApi(api)
);
}

Expand Down Expand Up @@ -236,27 +236,19 @@ public override Task<int> Replace(ComplexFormComponent beforeComponent, ComplexF
}
}

private class ComplexFormComponentsDiffApi(Entry afterEntry, IMiniLcmApi api) : IOrderableCollectionDiffApi<ComplexFormComponent>
private class ComplexFormComponentsDiffApi(IMiniLcmApi api) : IOrderableCollectionDiffApi<ComplexFormComponent, (Guid, Guid, Guid?)>
{
public Guid GetId(ComplexFormComponent component)
public (Guid, Guid, Guid?) GetId(ComplexFormComponent component)
{
// we can't use the ID as there's none defined by Fw so it won't work as a sync key
return component.ComponentSenseId ?? component.ComponentEntryId;
}

private BetweenPosition<ComplexFormComponent> MapBackToEntities(BetweenPosition between)
{
var previous = between!.Previous is null ? null : afterEntry.Components.Find(c => GetId(c) == between.Previous);
var next = between!.Next is null ? null : afterEntry.Components.Find(c => GetId(c) == between.Next);
return new BetweenPosition<ComplexFormComponent>(previous, next);
return (component.ComplexFormEntryId, component.ComponentEntryId, component.ComponentSenseId);
}

public async Task<int> Add(ComplexFormComponent after, BetweenPosition between)
public async Task<int> Add(ComplexFormComponent after, BetweenPosition<ComplexFormComponent> between)
{
var betweenComponents = MapBackToEntities(between);
try
{
await api.CreateComplexFormComponent(after, betweenComponents);
await api.CreateComplexFormComponent(after, between);
}
catch (NotFoundException)
{
Expand All @@ -265,10 +257,9 @@ public async Task<int> Add(ComplexFormComponent after, BetweenPosition between)
return 1;
}

public async Task<int> Move(ComplexFormComponent component, BetweenPosition between)
public async Task<int> Move(ComplexFormComponent component, BetweenPosition<ComplexFormComponent> between)
{
var betweenComponents = MapBackToEntities(between);
await api.MoveComplexFormComponent(component, betweenComponents);
await api.MoveComplexFormComponent(component, between);
return 1;
}

Expand All @@ -290,17 +281,22 @@ public Task<int> Replace(ComplexFormComponent beforeComponent, ComplexFormCompon
}
}

private class SensesDiffApi(IMiniLcmApi api, Guid entryId) : IOrderableCollectionDiffApi<Sense>
private class SensesDiffApi(IMiniLcmApi api, Guid entryId) : IOrderableCollectionDiffApi<Sense, Guid>
{
public async Task<int> Add(Sense sense, BetweenPosition between)
public Guid GetId(Sense sense)
{
return sense.Id;
}

public async Task<int> Add(Sense sense, BetweenPosition<Sense> between)
{
await api.CreateSense(entryId, sense, between);
await api.CreateSense(entryId, sense, new BetweenPosition(between.Previous?.Id, between.Next?.Id));
return 1;
}

public async Task<int> Move(Sense sense, BetweenPosition between)
public async Task<int> Move(Sense sense, BetweenPosition<Sense> between)
{
await api.MoveSense(entryId, sense.Id, between);
await api.MoveSense(entryId, sense.Id, new BetweenPosition(between.Previous?.Id, between.Next?.Id));
return 1;
}

Expand Down
15 changes: 10 additions & 5 deletions backend/FwLite/MiniLcm/SyncHelpers/ExampleSentenceSync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,22 @@ public override Guid GetId(Translation value)
}
}

private class ExampleSentencesDiffApi(IMiniLcmApi api, Guid entryId, Guid senseId) : IOrderableCollectionDiffApi<ExampleSentence>
private class ExampleSentencesDiffApi(IMiniLcmApi api, Guid entryId, Guid senseId) : IOrderableCollectionDiffApi<ExampleSentence, Guid>
{
public async Task<int> Add(ExampleSentence afterExampleSentence, BetweenPosition between)
public Guid GetId(ExampleSentence value)
{
await api.CreateExampleSentence(entryId, senseId, afterExampleSentence, between);
return value.Id;
}

public async Task<int> Add(ExampleSentence afterExampleSentence, BetweenPosition<ExampleSentence> between)
{
await api.CreateExampleSentence(entryId, senseId, afterExampleSentence, new BetweenPosition(between.Previous?.Id, between.Next?.Id));
return 1;
}

public async Task<int> Move(ExampleSentence example, BetweenPosition between)
public async Task<int> Move(ExampleSentence example, BetweenPosition<ExampleSentence> between)
{
await api.MoveExampleSentence(entryId, senseId, example.Id, between);
await api.MoveExampleSentence(entryId, senseId, example.Id, new BetweenPosition(between.Previous?.Id, between.Next?.Id));
return 1;
}

Expand Down
Loading
Loading