Skip to content

Commit b1e6a41

Browse files
hahn-kevmyieye
andauthored
Move and handle writing systems effectively (#1890)
* create move WritingSystem api with test * implement move writing system for FwData * implement moving ws in LcmCrdt * handle diff orderable writing systems * Mark WritingSystem.Order as MiniLcmInternal --------- Co-authored-by: Tim Haasdyk <tim_haasdyk@sil.org>
1 parent a0be52c commit b1e6a41

26 files changed

Lines changed: 411 additions & 48 deletions

backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using SIL.LCModel.Core.WritingSystems;
2020
using SIL.LCModel.DomainServices;
2121
using SIL.LCModel.Infrastructure;
22+
using CollectionExtensions = SIL.Extensions.CollectionExtensions;
2223

2324
namespace FwDataMiniLcmBridge.Api;
2425

@@ -94,7 +95,7 @@ public Task<WritingSystems> GetWritingSystems()
9495
{
9596
Vernacular = WritingSystemContainer.CurrentVernacularWritingSystems.Select((definition, index) =>
9697
FromLcmWritingSystem(definition, index, WritingSystemType.Vernacular)).ToArray(),
97-
Analysis = Cache.ServiceLocator.WritingSystems.CurrentAnalysisWritingSystems.Select((definition, index) =>
98+
Analysis = WritingSystemContainer.CurrentAnalysisWritingSystems.Select((definition, index) =>
9899
FromLcmWritingSystem(definition, index, WritingSystemType.Analysis)).ToArray()
99100
};
100101
CompleteExemplars(writingSystems);
@@ -117,15 +118,15 @@ private WritingSystem FromLcmWritingSystem(CoreWritingSystemDefinition ws, int i
117118
};
118119
}
119120

120-
public async Task<WritingSystem> GetWritingSystem(WritingSystemId id, WritingSystemType type)
121+
public async Task<WritingSystem?> GetWritingSystem(WritingSystemId id, WritingSystemType type)
121122
{
122123
var writingSystems = await GetWritingSystems();
123124
return type switch
124125
{
125126
WritingSystemType.Vernacular => writingSystems.Vernacular.FirstOrDefault(ws => ws.WsId == id),
126127
WritingSystemType.Analysis => writingSystems.Analysis.FirstOrDefault(ws => ws.WsId == id),
127128
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
128-
} ?? throw new NullReferenceException($"unable to find writing system with id {id}");
129+
};
129130
}
130131

131132
internal void CompleteExemplars(WritingSystems writingSystems)
@@ -149,7 +150,7 @@ internal void CompleteExemplars(WritingSystems writingSystems)
149150
}
150151
}
151152

152-
public async Task<WritingSystem> CreateWritingSystem(WritingSystem writingSystem)
153+
public async Task<WritingSystem> CreateWritingSystem(WritingSystem writingSystem, BetweenPosition<WritingSystemId?>? between = null)
153154
{
154155
var type = writingSystem.Type;
155156
var exitingWs = type == WritingSystemType.Analysis ? Cache.ServiceLocator.WritingSystems.AnalysisWritingSystems : Cache.ServiceLocator.WritingSystems.VernacularWritingSystems;
@@ -158,10 +159,9 @@ public async Task<WritingSystem> CreateWritingSystem(WritingSystem writingSystem
158159
throw new DuplicateObjectException($"Writing system {writingSystem.WsId.Code} already exists");
159160
}
160161
CoreWritingSystemDefinition? ws = null;
161-
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Writing System",
162+
await Cache.DoUsingNewOrCurrentUOW("Create Writing System",
162163
"Remove writing system",
163-
Cache.ServiceLocator.ActionHandler,
164-
() =>
164+
async () =>
165165
{
166166
Cache.ServiceLocator.WritingSystemManager.GetOrSet(writingSystem.WsId.Code, out ws);
167167
ws.Abbreviation = writingSystem.Abbreviation;
@@ -176,6 +176,9 @@ public async Task<WritingSystem> CreateWritingSystem(WritingSystem writingSystem
176176
default:
177177
throw new ArgumentOutOfRangeException(nameof(type), type, null);
178178
}
179+
180+
if (between is not null && (between.Previous is not null || between.Next is not null))
181+
await MoveWritingSystem(writingSystem.WsId, type, between);
179182
});
180183
if (ws is null) throw new InvalidOperationException("Writing system not found");
181184
var index = type switch
@@ -205,7 +208,7 @@ await Cache.DoUsingNewOrCurrentUOW("Update WritingSystem",
205208
update.Apply(updateProxy);
206209
return ValueTask.CompletedTask;
207210
});
208-
return await GetWritingSystem(id, type);
211+
return await GetWritingSystem(id, type) ?? throw new NullReferenceException($"unable to find writing system with id {id}");
209212
}
210213

211214
public async Task<WritingSystem> UpdateWritingSystem(WritingSystem before, WritingSystem after, IMiniLcmApi? api = null)
@@ -219,6 +222,61 @@ await Cache.DoUsingNewOrCurrentUOW("Update WritingSystem",
219222
return await GetWritingSystem(after.WsId, after.Type) ?? throw new NullReferenceException($"unable to find {after.Type} writing system with id {after.WsId}");
220223
}
221224

225+
public async Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition<WritingSystemId?> between)
226+
{
227+
var wsToUpdate = GetLexWritingSystem(id, type);
228+
if (wsToUpdate is null) throw new NullReferenceException($"unable to find writing system with id {id}");
229+
var previousWs = between.Previous is null ? null : GetLexWritingSystem(between.Previous.Value, type);
230+
var nextWs = between.Next is null ? null : GetLexWritingSystem(between.Next.Value, type);
231+
if (nextWs is null && previousWs is null) throw new NullReferenceException($"unable to find writing system with id {between.Previous} or {between.Next}");
232+
await Cache.DoUsingNewOrCurrentUOW("Move WritingSystem",
233+
"Revert Move WritingSystem",
234+
() =>
235+
{
236+
var exitingWs = type == WritingSystemType.Analysis
237+
? WritingSystemContainer.AnalysisWritingSystems
238+
: WritingSystemContainer.VernacularWritingSystems;
239+
var currentExistingWs = type == WritingSystemType.Analysis
240+
? WritingSystemContainer.CurrentAnalysisWritingSystems
241+
: WritingSystemContainer.CurrentVernacularWritingSystems;
242+
MoveWs(wsToUpdate, previousWs, nextWs, exitingWs);
243+
MoveWs(wsToUpdate, previousWs, nextWs, currentExistingWs);
244+
245+
void MoveWs(CoreWritingSystemDefinition ws,
246+
CoreWritingSystemDefinition? previous,
247+
CoreWritingSystemDefinition? next,
248+
ICollection<CoreWritingSystemDefinition> list)
249+
{
250+
var index = -1;
251+
if (previous is not null)
252+
{
253+
index = CollectionExtensions.IndexOf(list, previous);
254+
if (index >= 0) index++;
255+
}
256+
257+
if (index < 0 && next is not null)
258+
{
259+
index = CollectionExtensions.IndexOf(list, next);
260+
}
261+
262+
if (index < 0)
263+
throw new InvalidOperationException("unable to find writing system with id " + between.Previous + " or " + between.Next);
264+
265+
LcmHelpers.AddOrMoveInList(list, index, ws);
266+
}
267+
268+
return ValueTask.CompletedTask;
269+
});
270+
}
271+
272+
private CoreWritingSystemDefinition? GetLexWritingSystem(WritingSystemId id, WritingSystemType type)
273+
{
274+
var exitingWs = type == WritingSystemType.Analysis
275+
? WritingSystemContainer.AnalysisWritingSystems
276+
: WritingSystemContainer.VernacularWritingSystems;
277+
return exitingWs.FirstOrDefault(ws => ws.Id == id);
278+
}
279+
222280
public IAsyncEnumerable<PartOfSpeech> GetPartsOfSpeech()
223281
{
224282
return PartOfSpeechRepository

backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
using System.Globalization;
22
using MiniLcm.Culture;
33
using MiniLcm.Models;
4+
using SIL.Extensions;
45
using SIL.LCModel;
56
using SIL.LCModel.Core.KernelInterfaces;
67
using SIL.LCModel.Core.Text;
8+
using SIL.LCModel.Core.WritingSystems;
79

810
namespace FwDataMiniLcmBridge.Api;
911

@@ -289,4 +291,41 @@ internal static void SetString(this ITsMultiString multiString, FwDataMiniLcmApi
289291
multiString.set_String(writingSystemHandle, tsString);
290292
}
291293
}
294+
295+
//mostly a copy of method in SIL.FieldWorks.FwCoreDlgs.FwWritingSystemSetupModel
296+
internal static void AddOrMoveInList(
297+
ICollection<CoreWritingSystemDefinition> allWritingSystems,
298+
int desiredIndex,
299+
CoreWritingSystemDefinition workingWs
300+
)
301+
{
302+
if (desiredIndex < 0) throw new ArgumentOutOfRangeException(nameof(desiredIndex), desiredIndex, "desiredIndex must be >= 0");
303+
304+
// copy original contents into a list
305+
var updatedList = new List<CoreWritingSystemDefinition>(allWritingSystems);
306+
var ws = updatedList.Find(listItem =>
307+
{
308+
if (ReferenceEquals(listItem, workingWs)) return true;
309+
var workingTag = string.IsNullOrEmpty(workingWs.Id) ? workingWs.LanguageTag : workingWs.Id;
310+
var listItemTag = string.IsNullOrEmpty(listItem.Id) ? listItem.LanguageTag : listItem.Id;
311+
return string.Equals(listItemTag, workingTag);
312+
});
313+
314+
if (ws != null)
315+
{
316+
updatedList.Remove(ws);
317+
}
318+
319+
if (desiredIndex > updatedList.Count)
320+
{
321+
updatedList.Add(workingWs);
322+
}
323+
else
324+
{
325+
updatedList.Insert(desiredIndex, workingWs);
326+
}
327+
328+
allWritingSystems.Clear();
329+
allWritingSystems.AddRange(updatedList);
330+
}
292331
}

backend/FwLite/FwLiteProjectSync.Tests/Import/ResumableTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,9 @@ Task<Publication> IMiniLcmWriteApi.CreatePublication(Publication publication)
203203
ResumableTests.MaybeThrowRandom(random, 0.2);
204204
return _api.CreatePublication(publication);
205205
}
206-
Task<WritingSystem> IMiniLcmWriteApi.CreateWritingSystem(WritingSystem writingSystems)
206+
Task<WritingSystem> IMiniLcmWriteApi.CreateWritingSystem(WritingSystem writingSystems, BetweenPosition<WritingSystemId?>? between)
207207
{
208208
ResumableTests.MaybeThrowRandom(random, 0.2);
209-
return _api.CreateWritingSystem(writingSystems);
209+
return _api.CreateWritingSystem(writingSystems, between);
210210
}
211211
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using FwLiteProjectSync.Tests.Fixtures;
2+
using MiniLcm.Models;
3+
4+
namespace FwLiteProjectSync.Tests;
5+
6+
public class WritingSystemSyncTests : IClassFixture<SyncFixture>, IAsyncLifetime
7+
{
8+
9+
private readonly SyncFixture _fixture;
10+
private readonly CrdtFwdataProjectSyncService _syncService;
11+
12+
public WritingSystemSyncTests(SyncFixture fixture)
13+
{
14+
_fixture = fixture;
15+
_syncService = _fixture.SyncService;
16+
}
17+
18+
public async Task InitializeAsync()
19+
{
20+
await _fixture.FwDataApi.CreateEntry(new Entry()
21+
{
22+
Id = Guid.NewGuid(),
23+
LexemeForm = { { "en", "Pineapple" } },
24+
});
25+
await _fixture.FwDataApi.CreateWritingSystem(new WritingSystem()
26+
{
27+
Id = Guid.NewGuid(),
28+
Type = WritingSystemType.Vernacular,
29+
WsId = new WritingSystemId("fr"),
30+
Name = "French",
31+
Abbreviation = "fr",
32+
Font = "Arial"
33+
});
34+
await _syncService.Sync(_fixture.CrdtApi, _fixture.FwDataApi);
35+
}
36+
37+
public async Task DisposeAsync()
38+
{
39+
await foreach (var entry in _fixture.FwDataApi.GetAllEntries())
40+
{
41+
await _fixture.FwDataApi.DeleteEntry(entry.Id);
42+
}
43+
44+
foreach (var entry in await _fixture.CrdtApi.GetAllEntries().ToArrayAsync())
45+
{
46+
await _fixture.CrdtApi.DeleteEntry(entry.Id);
47+
}
48+
}
49+
50+
[Fact]
51+
public async Task SyncWs_UpdatesOrder()
52+
{
53+
var en = await _fixture.FwDataApi.GetWritingSystem("en", WritingSystemType.Vernacular);
54+
en.Should().NotBeNull();
55+
en.Order.Should().Be(0);
56+
var fr = await _fixture.FwDataApi.GetWritingSystem("fr", WritingSystemType.Vernacular);
57+
fr.Should().NotBeNull();
58+
fr.Order.Should().Be(1);
59+
await _fixture.FwDataApi.MoveWritingSystem("fr", WritingSystemType.Vernacular, new(null, "en"));
60+
fr = await _fixture.FwDataApi.GetWritingSystem("fr", WritingSystemType.Vernacular);
61+
fr.Should().NotBeNull();
62+
fr.Order.Should().Be(0);
63+
var crdtFr = await _fixture.CrdtApi.GetWritingSystem("fr", WritingSystemType.Vernacular);
64+
crdtFr.Should().NotBeNull();
65+
crdtFr.Order.Should().Be(1);
66+
67+
await _syncService.Sync(_fixture.CrdtApi, _fixture.FwDataApi);
68+
69+
fr = await _fixture.FwDataApi.GetWritingSystem("fr", WritingSystemType.Vernacular);
70+
fr.Should().NotBeNull();
71+
fr.Order.Should().Be(0);
72+
73+
var crdtEn = await _fixture.CrdtApi.GetWritingSystem("en", WritingSystemType.Vernacular);
74+
crdtEn.Should().NotBeNull();
75+
var actualFr = await _fixture.CrdtApi.GetWritingSystem("fr", WritingSystemType.Vernacular);
76+
actualFr.Should().NotBeNull();
77+
actualFr.Order.Should().BeLessThan(crdtEn.Order);
78+
}
79+
}

backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ public void Dispose()
1818

1919
public record DryRunRecord(string Method, string Description);
2020

21-
public Task<WritingSystem> CreateWritingSystem(WritingSystem writingSystem)
21+
public Task<WritingSystem> CreateWritingSystem(WritingSystem writingSystem, BetweenPosition<WritingSystemId?>? position = null)
2222
{
23-
DryRunRecords.Add(new DryRunRecord(nameof(CreateWritingSystem), $"Create writing system {writingSystem.Type}"));
23+
DryRunRecords.Add(new DryRunRecord(nameof(CreateWritingSystem),
24+
$"Create writing system {writingSystem.Type} between {position?.Previous} and {position?.Next}"));
2425
return Task.FromResult(writingSystem);
2526
}
2627

@@ -45,6 +46,12 @@ public Task<WritingSystem> UpdateWritingSystem(WritingSystem before, WritingSyst
4546
return Task.FromResult(after);
4647
}
4748

49+
public async Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition<WritingSystemId?> between)
50+
{
51+
DryRunRecords.Add(new DryRunRecord(nameof(MoveWritingSystem), $"Move writing system {id} between {between.Previous} and {between.Next}"));
52+
await Task.CompletedTask;
53+
}
54+
4855
public Task<PartOfSpeech> CreatePartOfSpeech(PartOfSpeech partOfSpeech)
4956
{
5057
DryRunRecords.Add(new DryRunRecord(nameof(CreatePartOfSpeech), $"Create part of speech {partOfSpeech.Name}"));

backend/FwLite/FwLiteProjectSync/Import/ResumableImportApi.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Runtime.CompilerServices;
22
using MiniLcm;
33
using MiniLcm.Models;
4+
using MiniLcm.SyncHelpers;
45

56
namespace FwLiteProjectSync.Import;
67

@@ -65,9 +66,9 @@ async Task<Publication> IMiniLcmWriteApi.CreatePublication(Publication publicati
6566
{
6667
return await HasCreated(publication, _api.GetPublications(), () => _api.CreatePublication(publication));
6768
}
68-
async Task<WritingSystem> IMiniLcmWriteApi.CreateWritingSystem(WritingSystem writingSystem)
69+
async Task<WritingSystem> IMiniLcmWriteApi.CreateWritingSystem(WritingSystem writingSystem, BetweenPosition<WritingSystemId?>? between = null)
6970
{
70-
return await HasCreated(writingSystem, AsyncWs(), () => _api.CreateWritingSystem(writingSystem), ws => ws.Type + ws.WsId.Code);
71+
return await HasCreated(writingSystem, AsyncWs(), () => _api.CreateWritingSystem(writingSystem, between), ws => ws.Type + ws.WsId.Code);
7172
}
7273

7374
private async IAsyncEnumerable<WritingSystem> AsyncWs()

backend/FwLite/LcmCrdt.Tests/Changes/RegressionDeserializationData.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,11 @@
277277
},
278278
"EntityId": "e03826fb-e6d2-6e3c-1f43-9fc6a0f6c4c6"
279279
},
280+
{
281+
"$type": "SetOrderChange:WritingSystem",
282+
"Order": 0.4870418422144669,
283+
"EntityId": "9d3e9006-c4b4-4316-6628-faeede75af40"
284+
},
280285
{
281286
"$type": "SetOrderChange:Sense",
282287
"Order": 0.4870418422144669,

backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,9 @@ private static IEnumerable<ChangeWithDependencies> GetAllChanges()
199199
var setComplexFormComponentOrderChange = new LcmCrdt.Changes.SetOrderChange<ComplexFormComponent>(complexFormComponent.Id, 10);
200200
yield return new ChangeWithDependencies(setComplexFormComponentOrderChange, [createComplexFormComponentChange]);
201201

202+
var setWritingSystemOrderChange = new LcmCrdt.Changes.SetOrderChange<WritingSystem>(writingSystem.Id, 10);
203+
yield return new ChangeWithDependencies(setWritingSystemOrderChange, [createWritingSystemChange]);
204+
202205
var publication = new Publication { Id = Guid.NewGuid(), Name = { { "en", "Main" } } };
203206
var createPublicationChange = new CreatePublicationChange(publication.Id, publication.Name);
204207
yield return new ChangeWithDependencies(createPublicationChange);

backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@
171171
{
172172
DerivedType: SetOrderChange<ComplexFormComponent>,
173173
TypeDiscriminator: SetOrderChange:ComplexFormComponent
174+
},
175+
{
176+
DerivedType: SetOrderChange<WritingSystem>,
177+
TypeDiscriminator: SetOrderChange:WritingSystem
174178
}
175179
],
176180
IgnoreUnrecognizedTypeDiscriminators: false,

backend/FwLite/LcmCrdt/Changes/SetOrderChange.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace LcmCrdt.Changes;
66

77
public class SetOrderChange<T>(Guid entityId, double order) : EditChange<T>(entityId), IPolyType
8-
where T : class, IOrderable
8+
where T : class, IOrderableNoId
99
{
1010
public static string TypeName => $"{nameof(SetOrderChange<T>)}:" + typeof(T).Name;
1111

0 commit comments

Comments
 (0)