Skip to content

Commit 7e413ca

Browse files
myieyeclaude
andcommitted
Enforce write-method validation coverage in the MiniLcm validation wrapper
The validation wrapper auto-forwarded the whole IMiniLcmApi via BeaKona, so any write method without a matching hand-written override silently forwarded unvalidated. That is how CreateEntry drifted out of validation: its override took (Entry) while the interface member is (Entry, CreateEntryOptions?), so the generated 2-arg forwarder bypassed it entirely (#2362). Re-target BeaKona at IMiniLcmReadApi only (like the write-normalization wrapper) and hand-write every write method, so a new/renamed write becomes a compile error instead of a silent unvalidated forward. Behavior is unchanged: the same methods validate as before, and the sync-write surface (CreateEntry, CreatePublication, CreateMorphType and all JsonPatch updates) deliberately stays pass-through because sync/import writes empty FLEx MultiStrings the validators would reject. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f58948e commit 7e413ca

1 file changed

Lines changed: 261 additions & 6 deletions

File tree

backend/FwLite/MiniLcm/Validators/MiniLcmApiValidationWrapper.cs

Lines changed: 261 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using MiniLcm;
2+
using MiniLcm.Media;
23
using MiniLcm.Models;
34
using MiniLcm.SyncHelpers;
45
using MiniLcm.Wrappers;
@@ -15,14 +16,29 @@ public IMiniLcmApi Create(IMiniLcmApi api)
1516
}
1617
}
1718

19+
/// <summary>
20+
/// Validates models on write before forwarding to the inner API.
21+
///
22+
/// Reads are auto-forwarded via BeaKona.AutoInterface. Writes are hand-written so that adding
23+
/// an IMiniLcmWriteApi method without choosing how it validates is a compile error rather than
24+
/// a silent unvalidated forward - which is how CreateEntry drifted out of validation (#2362).
25+
///
26+
/// CreateEntry, CreatePublication, CreateMorphType and all JsonPatch-based updates deliberately
27+
/// pass through unvalidated: sync/import wraps the api in this validator and writes empty
28+
/// MultiStrings from FLEx that those validators would reject, so validating here breaks import.
29+
/// </summary>
1830
public partial class MiniLcmApiValidationWrapper(
1931
IMiniLcmApi api,
2032
MiniLcmValidators validators) : IMiniLcmApi
2133
{
22-
[BeaKona.AutoInterface(IncludeBaseInterfaces = true, MemberMatch = BeaKona.MemberMatchTypes.Any)]
2334
private readonly IMiniLcmApi _api = api;
2435

25-
// ********** Overrides go here **********
36+
// BeaKona.AutoInterface only forwards IMiniLcmReadApi methods. IMiniLcmWriteApi methods are
37+
// NOT auto-forwarded, ensuring every write method is hand-written below (see class summary).
38+
[BeaKona.AutoInterface]
39+
private IMiniLcmReadApi ReadApi => _api;
40+
41+
#region WritingSystem
2642

2743
public async Task<WritingSystem> CreateWritingSystem(WritingSystem writingSystem, BetweenPosition<WritingSystemId?>? between = null)
2844
{
@@ -42,42 +58,122 @@ public async Task<WritingSystem> UpdateWritingSystem(WritingSystem before, Writi
4258
return await _api.UpdateWritingSystem(before, after, api ?? this);
4359
}
4460

61+
public Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition<WritingSystemId?> between)
62+
{
63+
return _api.MoveWritingSystem(id, type, between);
64+
}
65+
66+
#endregion
67+
68+
#region PartOfSpeech
69+
4570
public async Task<PartOfSpeech> CreatePartOfSpeech(PartOfSpeech partOfSpeech)
4671
{
4772
await validators.ValidateAndThrow(partOfSpeech);
4873
return await _api.CreatePartOfSpeech(partOfSpeech);
4974
}
5075

51-
public async Task<PartOfSpeech> UpdatePartOfSpeech(PartOfSpeech before, PartOfSpeech after, IMiniLcmApi? api)
76+
public Task<PartOfSpeech> UpdatePartOfSpeech(Guid id, UpdateObjectInput<PartOfSpeech> update)
77+
{
78+
return _api.UpdatePartOfSpeech(id, update);
79+
}
80+
81+
public async Task<PartOfSpeech> UpdatePartOfSpeech(PartOfSpeech before, PartOfSpeech after, IMiniLcmApi? api = null)
5282
{
5383
await validators.ValidateAndThrow(after);
5484
return await _api.UpdatePartOfSpeech(before, after, api ?? this);
5585
}
5686

87+
public Task DeletePartOfSpeech(Guid id)
88+
{
89+
return _api.DeletePartOfSpeech(id);
90+
}
91+
92+
#endregion
93+
94+
#region Publication
95+
96+
public Task<Publication> CreatePublication(Publication pub)
97+
{
98+
return _api.CreatePublication(pub);
99+
}
100+
101+
public Task<Publication> UpdatePublication(Guid id, UpdateObjectInput<Publication> update)
102+
{
103+
return _api.UpdatePublication(id, update);
104+
}
105+
106+
public Task<Publication> UpdatePublication(Publication before, Publication after, IMiniLcmApi? api = null)
107+
{
108+
return _api.UpdatePublication(before, after, api);
109+
}
110+
111+
public Task DeletePublication(Guid id)
112+
{
113+
return _api.DeletePublication(id);
114+
}
115+
116+
#endregion
117+
118+
#region SemanticDomain
119+
57120
public async Task<SemanticDomain> CreateSemanticDomain(SemanticDomain semanticDomain)
58121
{
59122
await validators.ValidateAndThrow(semanticDomain);
60123
return await _api.CreateSemanticDomain(semanticDomain);
61124
}
62125

126+
public Task<SemanticDomain> UpdateSemanticDomain(Guid id, UpdateObjectInput<SemanticDomain> update)
127+
{
128+
return _api.UpdateSemanticDomain(id, update);
129+
}
130+
63131
public async Task<SemanticDomain> UpdateSemanticDomain(SemanticDomain before, SemanticDomain after, IMiniLcmApi? api = null)
64132
{
65133
await validators.ValidateAndThrow(after);
66134
return await _api.UpdateSemanticDomain(before, after, api ?? this);
67135
}
68136

137+
public Task DeleteSemanticDomain(Guid id)
138+
{
139+
return _api.DeleteSemanticDomain(id);
140+
}
141+
142+
#endregion
143+
144+
#region ComplexFormType
145+
69146
public async Task<ComplexFormType> CreateComplexFormType(ComplexFormType complexFormType)
70147
{
71148
await validators.ValidateAndThrow(complexFormType);
72149
return await _api.CreateComplexFormType(complexFormType);
73150
}
74151

152+
public Task<ComplexFormType> UpdateComplexFormType(Guid id, UpdateObjectInput<ComplexFormType> update)
153+
{
154+
return _api.UpdateComplexFormType(id, update);
155+
}
156+
75157
public async Task<ComplexFormType> UpdateComplexFormType(ComplexFormType before, ComplexFormType after, IMiniLcmApi? api = null)
76158
{
77159
await validators.ValidateAndThrow(after);
78160
return await _api.UpdateComplexFormType(before, after, api ?? this);
79161
}
80162

163+
public Task DeleteComplexFormType(Guid id)
164+
{
165+
return _api.DeleteComplexFormType(id);
166+
}
167+
168+
#endregion
169+
170+
#region MorphType
171+
172+
public Task<MorphType> CreateMorphType(MorphType morphType)
173+
{
174+
return _api.CreateMorphType(morphType);
175+
}
176+
81177
public async Task<MorphType> UpdateMorphType(Guid id, UpdateObjectInput<MorphType> update)
82178
{
83179
await validators.ValidateAndThrow(update);
@@ -90,10 +186,18 @@ public async Task<MorphType> UpdateMorphType(MorphType before, MorphType after,
90186
return await _api.UpdateMorphType(before, after, api ?? this);
91187
}
92188

93-
public async Task<Entry> CreateEntry(Entry entry)
189+
#endregion
190+
191+
#region Entry
192+
193+
public Task<Entry> CreateEntry(Entry entry, CreateEntryOptions? options = null)
94194
{
95-
await validators.ValidateAndThrow(entry);
96-
return await _api.CreateEntry(entry);
195+
return _api.CreateEntry(entry, options);
196+
}
197+
198+
public Task<Entry> UpdateEntry(Guid id, UpdateObjectInput<Entry> update)
199+
{
200+
return _api.UpdateEntry(id, update);
97201
}
98202

99203
public async Task<Entry> UpdateEntry(Entry before, Entry after, IMiniLcmApi? api = null)
@@ -102,18 +206,96 @@ public async Task<Entry> UpdateEntry(Entry before, Entry after, IMiniLcmApi? api
102206
return await _api.UpdateEntry(before, after, api ?? this);
103207
}
104208

209+
public Task DeleteEntry(Guid id)
210+
{
211+
return _api.DeleteEntry(id);
212+
}
213+
214+
public Task<ComplexFormComponent> CreateComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition<ComplexFormComponent>? position = null)
215+
{
216+
return _api.CreateComplexFormComponent(complexFormComponent, position);
217+
}
218+
219+
public Task MoveComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition<ComplexFormComponent> between)
220+
{
221+
return _api.MoveComplexFormComponent(complexFormComponent, between);
222+
}
223+
224+
public Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent)
225+
{
226+
return _api.DeleteComplexFormComponent(complexFormComponent);
227+
}
228+
229+
public Task AddComplexFormType(Guid entryId, Guid complexFormTypeId)
230+
{
231+
return _api.AddComplexFormType(entryId, complexFormTypeId);
232+
}
233+
234+
public Task RemoveComplexFormType(Guid entryId, Guid complexFormTypeId)
235+
{
236+
return _api.RemoveComplexFormType(entryId, complexFormTypeId);
237+
}
238+
239+
public Task AddPublication(Guid entryId, Guid publicationId)
240+
{
241+
return _api.AddPublication(entryId, publicationId);
242+
}
243+
244+
public Task RemovePublication(Guid entryId, Guid publicationId)
245+
{
246+
return _api.RemovePublication(entryId, publicationId);
247+
}
248+
249+
#endregion
250+
251+
#region Sense
252+
105253
public async Task<Sense> CreateSense(Guid entryId, Sense sense, BetweenPosition? between = null)
106254
{
107255
await validators.ValidateAndThrow(sense);
108256
return await _api.CreateSense(entryId, sense, between);
109257
}
110258

259+
public Task<Sense> UpdateSense(Guid entryId, Guid senseId, UpdateObjectInput<Sense> update)
260+
{
261+
return _api.UpdateSense(entryId, senseId, update);
262+
}
263+
111264
public async Task<Sense> UpdateSense(Guid entryId, Sense before, Sense after, IMiniLcmApi? api = null)
112265
{
113266
await validators.ValidateAndThrow(after);
114267
return await _api.UpdateSense(entryId, before, after, api ?? this);
115268
}
116269

270+
public Task MoveSense(Guid entryId, Guid senseId, BetweenPosition between)
271+
{
272+
return _api.MoveSense(entryId, senseId, between);
273+
}
274+
275+
public Task DeleteSense(Guid entryId, Guid senseId)
276+
{
277+
return _api.DeleteSense(entryId, senseId);
278+
}
279+
280+
public Task AddSemanticDomainToSense(Guid senseId, SemanticDomain semanticDomain)
281+
{
282+
return _api.AddSemanticDomainToSense(senseId, semanticDomain);
283+
}
284+
285+
public Task RemoveSemanticDomainFromSense(Guid senseId, Guid semanticDomainId)
286+
{
287+
return _api.RemoveSemanticDomainFromSense(senseId, semanticDomainId);
288+
}
289+
290+
public Task SetSensePartOfSpeech(Guid senseId, Guid? partOfSpeechId)
291+
{
292+
return _api.SetSensePartOfSpeech(senseId, partOfSpeechId);
293+
}
294+
295+
#endregion
296+
297+
#region ExampleSentence
298+
117299
public async Task<ExampleSentence> CreateExampleSentence(Guid entryId,
118300
Guid senseId,
119301
ExampleSentence exampleSentence,
@@ -123,6 +305,14 @@ public async Task<ExampleSentence> CreateExampleSentence(Guid entryId,
123305
return await _api.CreateExampleSentence(entryId, senseId, exampleSentence, between);
124306
}
125307

308+
public Task<ExampleSentence> UpdateExampleSentence(Guid entryId,
309+
Guid senseId,
310+
Guid exampleSentenceId,
311+
UpdateObjectInput<ExampleSentence> update)
312+
{
313+
return _api.UpdateExampleSentence(entryId, senseId, exampleSentenceId, update);
314+
}
315+
126316
public async Task<ExampleSentence> UpdateExampleSentence(Guid entryId,
127317
Guid senseId,
128318
ExampleSentence before,
@@ -133,6 +323,71 @@ public async Task<ExampleSentence> UpdateExampleSentence(Guid entryId,
133323
return await _api.UpdateExampleSentence(entryId, senseId, before, after, api ?? this);
134324
}
135325

326+
public Task MoveExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId, BetweenPosition between)
327+
{
328+
return _api.MoveExampleSentence(entryId, senseId, exampleSentenceId, between);
329+
}
330+
331+
public Task DeleteExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId)
332+
{
333+
return _api.DeleteExampleSentence(entryId, senseId, exampleSentenceId);
334+
}
335+
336+
public Task AddTranslation(Guid entryId, Guid senseId, Guid exampleSentenceId, Translation translation)
337+
{
338+
return _api.AddTranslation(entryId, senseId, exampleSentenceId, translation);
339+
}
340+
341+
public Task RemoveTranslation(Guid entryId, Guid senseId, Guid exampleSentenceId, Guid translationId)
342+
{
343+
return _api.RemoveTranslation(entryId, senseId, exampleSentenceId, translationId);
344+
}
345+
346+
public Task UpdateTranslation(Guid entryId, Guid senseId, Guid exampleSentenceId, Guid translationId, UpdateObjectInput<Translation> update)
347+
{
348+
return _api.UpdateTranslation(entryId, senseId, exampleSentenceId, translationId, update);
349+
}
350+
351+
#endregion
352+
353+
#region CustomView
354+
355+
public Task<CustomView> CreateCustomView(CustomView customView)
356+
{
357+
return _api.CreateCustomView(customView);
358+
}
359+
360+
public Task<CustomView> UpdateCustomView(CustomView customView)
361+
{
362+
return _api.UpdateCustomView(customView);
363+
}
364+
365+
public Task DeleteCustomView(Guid id)
366+
{
367+
return _api.DeleteCustomView(id);
368+
}
369+
370+
#endregion
371+
372+
#region Bulk and Files
373+
374+
public Task BulkImportSemanticDomains(IAsyncEnumerable<SemanticDomain> semanticDomains)
375+
{
376+
return _api.BulkImportSemanticDomains(semanticDomains);
377+
}
378+
379+
public Task BulkCreateEntries(IAsyncEnumerable<Entry> entries)
380+
{
381+
return _api.BulkCreateEntries(entries);
382+
}
383+
384+
public Task<UploadFileResponse> SaveFile(Stream stream, LcmFileMetadata metadata)
385+
{
386+
return _api.SaveFile(stream, metadata);
387+
}
388+
389+
#endregion
390+
136391
void IDisposable.Dispose()
137392
{
138393
}

0 commit comments

Comments
 (0)