Skip to content

Commit a0be52c

Browse files
authored
Create new MiniLcmApi wrapper for normalizing strings (#1873)
* Create new WrapWith method for wrapping MiniLcmApi WrapWith wraps APIs in reverse order, last to first, so that the first one in its parameter list is the first one to receive data from JS. That way the ordering of factories in the array matches the order in which data coming from JS will flow down through the wrappers into the API. * Create normalization wrapper for MiniLcm search * Add tests for NormalizationWrapper Also hardcode normalization form to NFD, since we don't plan on ever changing it. * Fix BeaKona.AutoInterface invocation Turns out AutoInterface needs BeaKona.MemberMatchTypes.Any for it to actually use the implementations we were creating. So our wrappers weren't actually doing anything yet, and we didn't know.
1 parent 6a387db commit a0be52c

13 files changed

Lines changed: 195 additions & 13 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ public async Task DisposeAsync()
149149
internal partial class UnreliableApi(IMiniLcmApi api, Random random) : IMiniLcmApi
150150
{
151151

152-
[BeaKona.AutoInterface(IncludeBaseInterfaces = true)]
152+
[BeaKona.AutoInterface(IncludeBaseInterfaces = true, MemberMatch = BeaKona.MemberMatchTypes.Any)]
153153
private readonly IMiniLcmApi _api = api;
154154

155155
Task<PartOfSpeech> IMiniLcmWriteApi.CreatePartOfSpeech(PartOfSpeech partOfSpeech)

backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
namespace FwLiteProjectSync;
1515

1616
public class CrdtFwdataProjectSyncService(MiniLcmImport miniLcmImport, ILogger<CrdtFwdataProjectSyncService> logger,
17-
MiniLcmApiValidationWrapperFactory validationWrapperFactory)
17+
MiniLcmApiValidationWrapperFactory validationWrapperFactory, MiniLcmApiStringNormalizationWrapperFactory normalizationWrapperFactory)
1818
{
1919
public record DryRunSyncResult(
2020
int CrdtChanges,
@@ -51,8 +51,8 @@ public async Task<SyncResult> Sync(IMiniLcmApi crdtApi, FwDataMiniLcmApi fwdataA
5151

5252
private async Task<SyncResult> Sync(IMiniLcmApi crdtApi, IMiniLcmApi fwdataApi, bool dryRun, int entryCount, ProjectSnapshot? projectSnapshot)
5353
{
54-
crdtApi = validationWrapperFactory.Create(crdtApi);
55-
fwdataApi = validationWrapperFactory.Create(fwdataApi);
54+
crdtApi = normalizationWrapperFactory.Create(validationWrapperFactory.Create(crdtApi));
55+
fwdataApi = normalizationWrapperFactory.Create(validationWrapperFactory.Create(fwdataApi));
5656

5757
if (dryRun)
5858
{

backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace FwLiteProjectSync;
77

88
public partial class DryRunMiniLcmApi(IMiniLcmApi api) : IMiniLcmApi
99
{
10-
[BeaKona.AutoInterface(typeof(IMiniLcmReadApi))]
10+
[BeaKona.AutoInterface(typeof(IMiniLcmReadApi), MemberMatch = BeaKona.MemberMatchTypes.Any)]
1111
private readonly IMiniLcmApi _api = api;
1212

1313
public void Dispose()

backend/FwLite/FwLiteProjectSync/Import/ResumableImportApi.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace FwLiteProjectSync.Import;
66

77
public partial class ResumableImportApi(IMiniLcmApi api) : IMiniLcmApi
88
{
9-
[BeaKona.AutoInterface(IncludeBaseInterfaces = true)]
9+
[BeaKona.AutoInterface(IncludeBaseInterfaces = true, MemberMatch = BeaKona.MemberMatchTypes.Any)]
1010
private readonly IMiniLcmApi _api = api;
1111
private readonly Dictionary<string, Dictionary<string, object>> _createdObjects = new();
1212
private async ValueTask<T> HasCreated<T>(T value, IAsyncEnumerable<T> values, Func<Task<T>> create, [CallerMemberName] string typeName = "")

backend/FwLite/FwLiteShared/Services/MiniLcmApiNotifyWrapper.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
using MiniLcm;
44
using MiniLcm.Models;
55
using MiniLcm.SyncHelpers;
6+
using MiniLcm.Wrappers;
67

78
namespace FwLiteShared.Services;
89

9-
public class MiniLcmApiNotifyWrapperFactory(ProjectEventBus bus)
10+
public class MiniLcmApiNotifyWrapperFactory(ProjectEventBus bus) : IMiniLcmWrapperFactory
1011
{
1112
public IMiniLcmApi Create(IMiniLcmApi api, IProjectIdentifier project)
1213
{
@@ -19,7 +20,7 @@ public partial class MiniLcmApiNotifyWrapper(
1920
ProjectEventBus bus,
2021
IProjectIdentifier project) : IMiniLcmApi
2122
{
22-
[BeaKona.AutoInterface(IncludeBaseInterfaces = true)]
23+
[BeaKona.AutoInterface(IncludeBaseInterfaces = true, MemberMatch = BeaKona.MemberMatchTypes.Any)]
2324
private readonly IMiniLcmApi _api = api;
2425

2526
private PendingChangeNotifications? _pendingChanges;

backend/FwLite/FwLiteShared/Services/MiniLcmJsInvokable.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using MiniLcm.Media;
77
using MiniLcm.Models;
88
using MiniLcm.Validators;
9+
using MiniLcm.Wrappers;
910
using Reinforced.Typings.Attributes;
1011

1112
namespace FwLiteShared.Services;
@@ -18,7 +19,7 @@ public class MiniLcmJsInvokable(
1819
MiniLcmApiNotifyWrapperFactory notificationWrapperFactory,
1920
MiniLcmApiValidationWrapperFactory validationWrapperFactory) : IDisposable
2021
{
21-
private readonly IMiniLcmApi _wrappedApi = validationWrapperFactory.Create(notificationWrapperFactory.Create(api, project));
22+
private readonly IMiniLcmApi _wrappedApi = api.WrapWith([validationWrapperFactory, notificationWrapperFactory], project);
2223

2324
public record MiniLcmFeatures(bool? History, bool? Write, bool? OpenWithFlex, bool? Feedback, bool? Sync, bool? Audio);
2425
private bool SupportsSync => project.DataFormat == ProjectDataFormat.Harmony && api is CrdtMiniLcmApi;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using MiniLcm.Validators;
2+
using Moq;
3+
4+
namespace MiniLcm.Tests;
5+
6+
public class NormalizationTests
7+
{
8+
public IMiniLcmApi MockApi { get; init; }
9+
public IMiniLcmApi NormalizingApi { get; init; }
10+
11+
public const string NFCString = "na\u00efve"; // "naïve" with U+00EF LATIN SMALL LETTER I WITH DIAERESIS
12+
public const string NFDString = "na\u0069\u0308ve"; // "naïve" with U+0069 LATIN SMALL LETTER I + U+0308 COMBINING DIAERESIS
13+
14+
public FilterQueryOptions NFCOptions = new()
15+
{
16+
Exemplar = new ExemplarOptions(NFCString, "en"),
17+
Filter = new Filtering.EntryFilter { GridifyFilter = NFCString },
18+
};
19+
20+
public QueryOptions NFCQueryOptions = new()
21+
{
22+
Exemplar = new ExemplarOptions(NFCString, "en"),
23+
Filter = new Filtering.EntryFilter { GridifyFilter = NFCString },
24+
};
25+
26+
public FilterQueryOptions NFDOptions = new()
27+
{
28+
Exemplar = new ExemplarOptions(NFDString, "en"),
29+
Filter = new Filtering.EntryFilter { GridifyFilter = NFDString },
30+
};
31+
32+
public NormalizationTests()
33+
{
34+
MockApi = Mock.Of<IMiniLcmApi>();
35+
// Mock.Get(MockApi).Setup(api => api.SearchEntries(It.IsAny<string>(), null)).Returns(new List<Entry>().ToAsyncEnumerable());
36+
var factory = new MiniLcmApiStringNormalizationWrapperFactory();
37+
NormalizingApi = factory.Create(MockApi);
38+
}
39+
40+
[Fact]
41+
public void SearchEntriesIsNormalized()
42+
{
43+
NormalizingApi.Should().BeOfType<MiniLcmApiStringNormalizationWrapper>();
44+
var results = NormalizingApi.SearchEntries(NFCString, null);
45+
Mock.Get(MockApi).Verify(api => api.SearchEntries(NFDString, null));
46+
}
47+
48+
[Fact]
49+
public void SearchEntriesWithQueryOptionsAreNormalized()
50+
{
51+
NormalizingApi.Should().BeOfType<MiniLcmApiStringNormalizationWrapper>();
52+
var results = NormalizingApi.SearchEntries(NFCString, NFCQueryOptions);
53+
Mock.Get(MockApi).Verify(api => api.SearchEntries(NFDString, It.Is<QueryOptions>(
54+
opt => opt.Exemplar!.Value == NFDOptions.Exemplar!.Value &&
55+
opt.Filter!.GridifyFilter == NFDOptions.Filter!.GridifyFilter)));
56+
}
57+
58+
[Fact]
59+
public void CountEntriesIsNormalized()
60+
{
61+
NormalizingApi.Should().BeOfType<MiniLcmApiStringNormalizationWrapper>();
62+
var results = NormalizingApi.CountEntries(NFCString, null);
63+
Mock.Get(MockApi).Verify(api => api.CountEntries(NFDString, null));
64+
}
65+
66+
[Fact]
67+
public void CountEntriesWithFilterQueryOptionsIsNormalized()
68+
{
69+
NormalizingApi.Should().BeOfType<MiniLcmApiStringNormalizationWrapper>();
70+
var results = NormalizingApi.CountEntries(NFCString, NFCOptions);
71+
Mock.Get(MockApi).Verify(api => api.CountEntries(NFDString, It.Is<FilterQueryOptions>(
72+
opt => opt.Exemplar!.Value == NFDOptions.Exemplar!.Value &&
73+
opt.Filter!.GridifyFilter == NFDOptions.Filter!.GridifyFilter)));
74+
}
75+
76+
[Fact]
77+
public void GetEntriesIsNormalized()
78+
{
79+
NormalizingApi.Should().BeOfType<MiniLcmApiStringNormalizationWrapper>();
80+
var results = NormalizingApi.GetEntries(NFCQueryOptions);
81+
Mock.Get(MockApi).Verify(api => api.GetEntries(It.Is<QueryOptions>(
82+
opt => opt.Exemplar!.Value == NFDOptions.Exemplar!.Value &&
83+
opt.Filter!.GridifyFilter == NFDOptions.Filter!.GridifyFilter)));
84+
}
85+
}

backend/FwLite/MiniLcm/Filtering/EntryFilter.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
using Gridify;
1+
using System.Linq.Expressions;
2+
using System.Text;
3+
using Gridify;
4+
using Gridify.Syntax;
25
using MiniLcm.Models;
36

47
namespace MiniLcm.Filtering;
@@ -53,4 +56,8 @@ public static object NormalizeEmptyToNull<T>(string value)
5356

5457
public string? GridifyFilter { get; set; }
5558

59+
public EntryFilter Normalized(NormalizationForm form)
60+
{
61+
return new() { GridifyFilter = GridifyFilter?.Normalize(form) };
62+
}
5663
}

backend/FwLite/MiniLcm/IMiniLcmReadApi.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Linq.Expressions;
2+
using System.Text;
23
using System.Text.Json.Serialization;
34
using MiniLcm.Filtering;
45
using MiniLcm.Media;
@@ -38,6 +39,10 @@ public record FilterQueryOptions(
3839
{
3940
public static FilterQueryOptions Default { get; } = new();
4041
public bool HasFilter => Filter is {GridifyFilter.Length: > 0 } || Exemplar is {Value.Length: > 0};
42+
public virtual FilterQueryOptions Normalized(NormalizationForm form)
43+
{
44+
return new(Exemplar?.Normalized(form), Filter?.Normalized(form));
45+
}
4146
}
4247

4348
public record QueryOptions(
@@ -52,6 +57,11 @@ public record QueryOptions(
5257
public const int DefaultCount = 1000;
5358
public SortOptions Order { get; init; } = Order ?? SortOptions.Default;
5459

60+
public override QueryOptions Normalized(NormalizationForm form)
61+
{
62+
return new(Order, Exemplar?.Normalized(form), Count, Offset, Filter?.Normalized(form));
63+
}
64+
5565
public IEnumerable<T> ApplyPaging<T>(IEnumerable<T> enumerable)
5666
{
5767
if (Offset > 0)
@@ -99,7 +109,13 @@ public record SortOptions(SortField Field, WritingSystemId WritingSystem = defau
99109
public static SortOptions Default { get; } = new(SortField.Headword, DefaultWritingSystem);
100110
}
101111

102-
public record ExemplarOptions(string Value, WritingSystemId WritingSystem);
112+
public record ExemplarOptions(string Value, WritingSystemId WritingSystem)
113+
{
114+
public ExemplarOptions Normalized(NormalizationForm form)
115+
{
116+
return new(Value.Normalize(form), WritingSystem);
117+
}
118+
}
103119

104120
[JsonConverter(typeof(JsonStringEnumConverter))]
105121
public enum SortField
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System.Text;
2+
using MiniLcm;
3+
using MiniLcm.Models;
4+
using MiniLcm.SyncHelpers;
5+
using MiniLcm.Wrappers;
6+
7+
namespace MiniLcm.Validators;
8+
9+
public class MiniLcmApiStringNormalizationWrapperFactory() : IMiniLcmWrapperFactory
10+
{
11+
public IMiniLcmApi Create(IMiniLcmApi api, IProjectIdentifier _unused) => Create(api);
12+
13+
public IMiniLcmApi Create(IMiniLcmApi api)
14+
{
15+
return new MiniLcmApiStringNormalizationWrapper(api);
16+
}
17+
}
18+
19+
public partial class MiniLcmApiStringNormalizationWrapper(
20+
IMiniLcmApi api) : IMiniLcmApi
21+
{
22+
public const NormalizationForm Form = NormalizationForm.FormD;
23+
24+
[BeaKona.AutoInterface(IncludeBaseInterfaces = true, MemberMatch = BeaKona.MemberMatchTypes.Any)]
25+
private readonly IMiniLcmApi _api = api;
26+
27+
// ********** Overrides go here **********
28+
29+
public IAsyncEnumerable<Entry> SearchEntries(string query, QueryOptions? options = null)
30+
{
31+
return _api.SearchEntries(query.Normalize(Form), options?.Normalized(Form));
32+
}
33+
34+
public Task<int> CountEntries(string? query = null, FilterQueryOptions? options = null)
35+
{
36+
return _api.CountEntries(query?.Normalize(Form), options?.Normalized(Form));
37+
}
38+
39+
public IAsyncEnumerable<Entry> GetEntries(QueryOptions? options = null)
40+
{
41+
return _api.GetEntries(options?.Normalized(Form));
42+
}
43+
44+
void IDisposable.Dispose()
45+
{
46+
}
47+
}

0 commit comments

Comments
 (0)