Skip to content

Commit

Permalink
Backend changes to make PartOfSpeech an object
Browse files Browse the repository at this point in the history
  • Loading branch information
rmunn committed Jan 7, 2025
1 parent bdace23 commit b285d06
Show file tree
Hide file tree
Showing 13 changed files with 95 additions and 50 deletions.
5 changes: 3 additions & 2 deletions backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -561,14 +561,15 @@ private ComplexFormComponent ToSenseReference(ILexSense componentSense, ILexEntr
private Sense FromLexSense(ILexSense sense)
{
var enWs = GetWritingSystemHandle("en");
var pos = sense.MorphoSyntaxAnalysisRA?.GetPartOfSpeech();
var s = new Sense
{
Id = sense.Guid,
EntryId = sense.Entry.Guid,
Gloss = FromLcmMultiString(sense.Gloss),
Definition = FromLcmMultiString(sense.Definition),
PartOfSpeech = sense.MorphoSyntaxAnalysisRA?.GetPartOfSpeech()?.Name.get_String(enWs).Text ?? "",
PartOfSpeechId = sense.MorphoSyntaxAnalysisRA?.GetPartOfSpeech()?.Guid,
PartOfSpeech = pos is null ? null : FromLcmPartOfSpeech(pos),
PartOfSpeechId = pos?.Guid,
SemanticDomains = sense.SemanticDomainsRC.Select(FromLcmSemanticDomain).ToList(),
ExampleSentences = sense.ExamplesOS.Select(sentence => FromLexExampleSentence(sense.Guid, sentence)).ToList()
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public override MultiString Gloss
set => throw new NotImplementedException();
}

public override string PartOfSpeech
public override PartOfSpeech? PartOfSpeech
{
get => throw new NotImplementedException();
set { }
Expand Down
17 changes: 9 additions & 8 deletions backend/FwLite/LcmCrdt.Tests/Changes/JsonPatchChangeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@ public void NewChangeIPatchDoc_ThrowsForRemoveAtIndex()
act.Should().Throw<NotSupportedException>();
}

[Fact]
public void NewPatchDoc_ThrowsForIndexBasedPath()
{
var patch = new JsonPatchDocument<Entry>();
patch.Replace(entry => entry.Senses[0].PartOfSpeech, "noun");
var act = () => new JsonPatchChange<Entry>(Guid.NewGuid(), patch);
act.Should().Throw<NotSupportedException>();
}
// TODO: Doesn't work now that Sense.PartOfSpeech is an object. Rewrite or toss?
// [Fact]
// public void NewPatchDoc_ThrowsForIndexBasedPath()
// {
// var patch = new JsonPatchDocument<Entry>();
// patch.Replace(entry => entry.Senses[0].PartOfSpeech, "noun");
// var act = () => new JsonPatchChange<Entry>(Guid.NewGuid(), patch);
// act.Should().Throw<NotSupportedException>();
// }
}
20 changes: 13 additions & 7 deletions backend/FwLite/LcmCrdt.Tests/JsonPatchSenseRewriteTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,20 @@ public class JsonPatchSenseRewriteTests
{
private JsonPatchDocument<MiniLcm.Models.Sense> _patchDocument = new() { Options = new JsonSerializerOptions(JsonSerializerDefaults.Web) };

private Sense _sense = new Sense()
private Sense _sense = MakeSense("test");

private static Sense MakeSense(string name)
{
Id = Guid.NewGuid(),
EntryId = Guid.NewGuid(),
PartOfSpeechId = Guid.NewGuid(),
PartOfSpeech = "test",
SemanticDomains = [new SemanticDomain() { Id = Guid.NewGuid(), Code = "test", Name = new MultiString() }],
};
var pos = new PartOfSpeech() { Id = Guid.NewGuid(), Name = {{ "en", name }} };
return new Sense()
{
Id = Guid.NewGuid(),
EntryId = Guid.NewGuid(),
PartOfSpeech = pos,
PartOfSpeechId = pos.Id,
SemanticDomains = [new SemanticDomain() { Id = Guid.NewGuid(), Code = "test", Name = new MultiString() }],
};
}

[Fact]
public void RewritePartOfSpeechChangesIntoSetPartOfSpeechChange()
Expand Down
4 changes: 2 additions & 2 deletions backend/FwLite/LcmCrdt/Changes/CreateSenseChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ private CreateSenseChange(Guid entityId, Guid entryId) : base(entityId)
public double Order { get; set; }
public MultiString? Definition { get; set; }
public MultiString? Gloss { get; set; }
public string? PartOfSpeech { get; set; }
public PartOfSpeech? PartOfSpeech { get; set; }
public Guid? PartOfSpeechId { get; set; }
public IList<SemanticDomain>? SemanticDomains { get; set; }

Expand All @@ -42,7 +42,7 @@ public override async ValueTask<Sense> NewEntity(Commit commit, ChangeContext co
Order = Order,
Definition = Definition ?? new MultiString(),
Gloss = Gloss ?? new MultiString(),
PartOfSpeech = PartOfSpeech ?? string.Empty,
PartOfSpeech = PartOfSpeech,
PartOfSpeechId = PartOfSpeechId,
SemanticDomains = SemanticDomains ?? [],
DeletedAt = await context.IsObjectDeleted(EntryId) ? commit.DateTime : (DateTime?)null
Expand Down
6 changes: 3 additions & 3 deletions backend/FwLite/LcmCrdt/Changes/SetPartOfSpeechChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ public override async ValueTask ApplyChange(Sense entity, ChangeContext context)
if (PartOfSpeechId is null)
{
entity.PartOfSpeechId = null;
entity.PartOfSpeech = string.Empty;
entity.PartOfSpeech = null;
return;
}

var partOfSpeech = await context.GetCurrent<PartOfSpeech>(PartOfSpeechId.Value);
if (partOfSpeech is null or { DeletedAt: not null })
{
entity.PartOfSpeechId = null;
entity.PartOfSpeech = string.Empty;
entity.PartOfSpeech = null;
return;
}
entity.PartOfSpeechId = partOfSpeech.Id;
entity.PartOfSpeech = partOfSpeech.Name["en"];
entity.PartOfSpeech = partOfSpeech;
}
}
4 changes: 2 additions & 2 deletions backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ private IEnumerable<IChange> CreateEntryChanges(Entry entry, Dictionary<Guid, Se
if (sense.PartOfSpeechId is not null && partsOfSpeech.TryGetValue(sense.PartOfSpeechId.Value, out var partOfSpeech))
{
sense.PartOfSpeechId = partOfSpeech.Id;
sense.PartOfSpeech = partOfSpeech.Name["en"] ?? string.Empty;
sense.PartOfSpeech = partOfSpeech;
}
if (sense.Order != default) // we don't anticipate this being necessary, so we'll be strict for now
throw new InvalidOperationException("Order should not be provided when creating a sense");
Expand Down Expand Up @@ -466,7 +466,7 @@ private async IAsyncEnumerable<IChange> CreateSenseChanges(Guid entryId, Sense s
{
var partOfSpeech = await PartsOfSpeech.FirstOrDefaultAsync(p => p.Id == sense.PartOfSpeechId);
sense.PartOfSpeechId = partOfSpeech?.Id;
sense.PartOfSpeech = partOfSpeech?.Name["en"] ?? string.Empty;
sense.PartOfSpeech = partOfSpeech;
}

yield return new CreateSenseChange(sense, entryId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ public static async Task<Entry> EntryReadyForCreation(this AutoFaker autoFaker,
foreach (var sense in entry.Senses)
{
sense.EntryId = entry.Id;
if (sense.PartOfSpeechId.HasValue)
if (sense.PartOfSpeechId.HasValue && sense.PartOfSpeech is null)
{
await api.CreatePartOfSpeech(new PartOfSpeech()
var pos = new PartOfSpeech()
{
Id = sense.PartOfSpeechId.Value,
Name = { { "en", sense.PartOfSpeech } }
});
Name = { { "en", "generated pos" } }
};
await api.CreatePartOfSpeech(pos);
sense.PartOfSpeech = pos;
}
foreach (var senseSemanticDomain in sense.SemanticDomains)
{
Expand Down
14 changes: 8 additions & 6 deletions backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,9 @@ public async Task CreateSense_WillCreateWithExistingDomains()
public async Task CreateSense_WontCreateMissingPartOfSpeech()
{
var senseId = Guid.NewGuid();
var partOfSpeechId = Guid.NewGuid();
var createdSense = await Api.CreateSense(Entry1Id,
new Sense() { Id = senseId, PartOfSpeech = "test", PartOfSpeechId = Guid.NewGuid(), });
new Sense() { Id = senseId, PartOfSpeech = null, PartOfSpeechId = partOfSpeechId, });
createdSense.Id.Should().Be(senseId);
createdSense.PartOfSpeechId.Should().BeNull("because the part of speech does not exist (or was deleted)");
}
Expand All @@ -377,7 +378,7 @@ public async Task CreateSense_WillCreateWthExistingPartOfSpeech()
var partOfSpeech = await Api.GetPartsOfSpeech().SingleOrDefaultAsync(pos => pos.Id == partOfSpeechId);
ArgumentNullException.ThrowIfNull(partOfSpeech);
var createdSense = await Api.CreateSense(Entry1Id,
new Sense() { Id = senseId, PartOfSpeech = "test", PartOfSpeechId = partOfSpeechId, });
new Sense() { Id = senseId, PartOfSpeech = partOfSpeech, PartOfSpeechId = partOfSpeechId, });
createdSense.Id.Should().Be(senseId);
createdSense.PartOfSpeechId.Should().Be(partOfSpeechId, "because the part of speech does exist");
}
Expand All @@ -386,7 +387,7 @@ public async Task CreateSense_WillCreateWthExistingPartOfSpeech()
public async Task UpdateSensePartOfSpeech()
{
var partOfSpeechId = Guid.NewGuid();
await Api.CreatePartOfSpeech(new PartOfSpeech() { Id = partOfSpeechId, Name = new MultiString() { { "en", "Adverb" } } });
var partOfSpeech = await Api.CreatePartOfSpeech(new PartOfSpeech() { Id = partOfSpeechId, Name = new MultiString() { { "en", "Adverb" } } });
var entry = await Api.CreateEntry(new Entry
{
LexemeForm = new MultiString
Expand All @@ -400,7 +401,7 @@ public async Task UpdateSensePartOfSpeech()
{
new Sense()
{
PartOfSpeech = "test",
PartOfSpeech = new PartOfSpeech() { Id = Guid.NewGuid(), Name = {{"en", "test"}} },
Definition = new MultiString
{
Values =
Expand All @@ -414,9 +415,10 @@ public async Task UpdateSensePartOfSpeech()
var updatedSense = await Api.UpdateSense(entry.Id,
entry.Senses[0].Id,
new UpdateObjectInput<Sense>()
.Set(e => e.PartOfSpeech, "updated")//should be ignored
.Set(e => e.PartOfSpeech, new PartOfSpeech() { Id = Guid.NewGuid(), Name = {{"en","updated"}} }) // should be ignored
.Set(e => e.PartOfSpeechId, partOfSpeechId));
updatedSense.PartOfSpeech.Should().Be("Adverb");
updatedSense.PartOfSpeech.Should().NotBeNull();
updatedSense.PartOfSpeech.Name.Should().Be("Adverb");
updatedSense.PartOfSpeechId.Should().Be(partOfSpeechId);
}

Expand Down
13 changes: 7 additions & 6 deletions backend/FwLite/MiniLcm.Tests/PartOfSpeechTestsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,23 @@ public async Task GetPartsOfSpeech_ReturnsAllPartsOfSpeech()
[Fact]
public async Task Sense_HasPartOfSpeech()
{
var entry = await Api.GetEntries().FirstAsync(e => e.Senses.Any(s => !string.IsNullOrEmpty(s.PartOfSpeech)));
var sense = entry.Senses.First(s => !string.IsNullOrEmpty(s.PartOfSpeech));
sense.PartOfSpeech.Should().NotBeNullOrEmpty();
var entry = await Api.GetEntries().FirstAsync(e => e.Senses.Any(s => s.PartOfSpeech is not null));
var sense = entry.Senses.First(s => s.PartOfSpeech is not null);
sense.PartOfSpeech.Should().NotBeNull();
sense.PartOfSpeechId.Should().NotBeNull();
// TODO: This test becomes meaningless now that PartOfSpeech is an object. Rewrite, or toss?
}

[Fact]
public async Task Sense_UpdatesPartOfSpeech()
{
var entry = await Api.GetEntries().FirstAsync(e => e.Senses.Any(s => !string.IsNullOrEmpty(s.PartOfSpeech)));
var sense = entry.Senses.First(s => !string.IsNullOrEmpty(s.PartOfSpeech));
var entry = await Api.GetEntries().FirstAsync(e => e.Senses.Any(s => s.PartOfSpeech is not null));
var sense = entry.Senses.First(s => s.PartOfSpeech is not null);
var newPartOfSpeech = await Api.GetPartsOfSpeech().FirstAsync(po => po.Id != sense.PartOfSpeechId);

var update = new UpdateObjectInput<Sense>()
//This is required for CRDTs, but not for FW
.Set(s => s.PartOfSpeech, newPartOfSpeech.Name["en"])
.Set(s => s.PartOfSpeech, newPartOfSpeech)
.Set(s => s.PartOfSpeechId, newPartOfSpeech.Id);
await Api.UpdateSense(entry.Id, sense.Id, update);

Expand Down
2 changes: 1 addition & 1 deletion backend/FwLite/MiniLcm/Models/Sense.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class Sense : IObjectWithId, IOrderable
public Guid EntryId { get; set; }
public virtual MultiString Definition { get; set; } = new();
public virtual MultiString Gloss { get; set; } = new();
public virtual string PartOfSpeech { get; set; } = string.Empty;
public virtual PartOfSpeech? PartOfSpeech { get; set; } = null;
public virtual Guid? PartOfSpeechId { get; set; }
public virtual IList<SemanticDomain> SemanticDomains { get; set; } = [];
public virtual IList<ExampleSentence> ExampleSentences { get; set; } = [];
Expand Down
34 changes: 26 additions & 8 deletions backend/LfClassicData/LfClassicMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,14 @@ public async IAsyncEnumerable<PartOfSpeech> GetPartsOfSpeech()

public async Task<PartOfSpeech?> GetPartOfSpeech(Guid id)
{
return await GetPartsOfSpeech().FirstOrDefaultAsync(pos => pos.Id == id);
var item = await dbContext.GetOptionListItemByGuid(projectCode, "grammatical-info", id);
return item is null ? null : ToPartOfSpeech(item);
}

public async Task<PartOfSpeech?> GetPartOfSpeech(string key)
{
var item = await dbContext.GetOptionListItemByKey(projectCode, "grammatical-info", key);
return item is null ? null : ToPartOfSpeech(item);
}

public async IAsyncEnumerable<SemanticDomain> GetSemanticDomains()
Expand Down Expand Up @@ -157,7 +164,7 @@ private async IAsyncEnumerable<Entry> Query(QueryOptions? options = null, string

await foreach (var entry in Entries.Aggregate(pipeline).ToAsyncEnumerable())
{
yield return ToEntry(entry);
yield return await ToEntry(entry); // TODO: Is there a way to turn this into a .Select()? Because "yield return await" looks a little unwieldy
}
}

Expand Down Expand Up @@ -230,28 +237,39 @@ private async IAsyncEnumerable<Entry> Query(QueryOptions? options = null, string
})));
}

private static Entry ToEntry(Entities.Entry entry)
private async Task<Entry> ToEntry(Entities.Entry entry)
{
List<Sense> senses;
if (entry.Senses is null)
{
senses = [];
}
else
{
var senseTasks = entry.Senses.OfType<Entities.Sense>().Select(sense => ToSense(entry.Guid, sense));
await Task.WhenAll(senseTasks);
senses = senseTasks.Select(task => task.Result).ToList();
}
return new Entry
{
Id = entry.Guid,
CitationForm = ToMultiString(entry.CitationForm),
LexemeForm = ToMultiString(entry.Lexeme),
Note = ToMultiString(entry.Note),
LiteralMeaning = ToMultiString(entry.LiteralMeaning),
Senses = entry.Senses?.OfType<Entities.Sense>().Select(sense => ToSense(entry.Guid,sense)).ToList() ?? [],
Senses = senses,
};
}

private static Sense ToSense(Guid entryId, Entities.Sense sense)
private async Task<Sense> ToSense(Guid entryId, Entities.Sense sense)
{
return new Sense
{
Id = sense.Guid,
EntryId = entryId,
Gloss = ToMultiString(sense.Gloss),
Definition = ToMultiString(sense.Definition),
PartOfSpeech = sense.PartOfSpeech?.Value ?? string.Empty,
PartOfSpeech = sense.PartOfSpeech is null ? null : await GetPartOfSpeech(sense.PartOfSpeech.Value),
SemanticDomains = (sense.SemanticDomain?.Values ?? [])
.Select(sd => new SemanticDomain { Id = Guid.Empty, Code = sd, Name = new MultiString { { "en", sd } } })
.ToList(),
Expand Down Expand Up @@ -317,7 +335,7 @@ private static SemanticDomain ToSemanticDomain(Entities.OptionListItem item)
{
var entry = await Entries.Find(e => e.Guid == id).FirstOrDefaultAsync();
if (entry is null) return null;
return ToEntry(entry);
return await ToEntry(entry);
}

public async Task<Sense?> GetSense(Guid entryId, Guid id)
Expand All @@ -326,7 +344,7 @@ private static SemanticDomain ToSemanticDomain(Entities.OptionListItem item)
if (entry is null) return null;
var sense = entry.Senses?.FirstOrDefault(s => s?.Guid == id);
if (sense is null) return null;
return ToSense(entryId, sense);
return await ToSense(entryId, sense);
}

public async Task<ExampleSentence?> GetExampleSentence(Guid entryId, Guid senseId, Guid id)
Expand Down
14 changes: 14 additions & 0 deletions backend/LfClassicData/ProjectDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,18 @@ public async Task<OptionListItem[]> GetOptionListItems(string projectCode, strin
if (result is null) return [];
return [..result.Items];
}

public async Task<OptionListItem?> GetOptionListItemByKey(string projectCode, string listCode, string key)
{
var collection = GetCollection<OptionListRecord>(projectCode, "optionlists");
var result = await collection.Find(e => e.Code == listCode).FirstOrDefaultAsync();
return result.Items.Find(item => item.Key == key);
}

public async Task<OptionListItem?> GetOptionListItemByGuid(string projectCode, string listCode, Guid guid)
{
var collection = GetCollection<OptionListRecord>(projectCode, "optionlists");
var result = await collection.Find(e => e.Code == listCode).FirstOrDefaultAsync();
return result.Items.Find(item => item.Guid == guid);
}
}

0 comments on commit b285d06

Please sign in to comment.