Skip to content

Commit

Permalink
allow creating semantic domains and referencing them in senses, rewri…
Browse files Browse the repository at this point in the history
…te json patch to change semantic domains of senses. Add tests for creating senses with and without semantic domains, and with and without part of speeches.
  • Loading branch information
hahn-kev committed Jun 13, 2024
1 parent e933d90 commit 192962b
Show file tree
Hide file tree
Showing 12 changed files with 363 additions and 25 deletions.
96 changes: 93 additions & 3 deletions backend/LcmCrdt.Tests/JsonPatchRewriteTests.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
using LcmCrdt.Changes;
using LcmCrdt.Objects;
using MiniLcm;
using SystemTextJsonPatch;
using SemanticDomain = LcmCrdt.Objects.SemanticDomain;
using Sense = LcmCrdt.Objects.Sense;

namespace LcmCrdt.Tests;

public class JsonPatchRewriteTests
{
private Sense _sense = new Sense()
{
Id = Guid.NewGuid(),
EntryId = Guid.NewGuid(),
PartOfSpeechId = Guid.NewGuid(),
PartOfSpeech = "test",
SemanticDomains = [new SemanticDomain() { Id = Guid.NewGuid(), Code = "test", Name = new MultiString() }],
};

[Fact]
public void RewritePartOfSpeechChangesIntoSetPartOfSpeechChange()
{
Expand All @@ -14,12 +25,14 @@ public void RewritePartOfSpeechChangesIntoSetPartOfSpeechChange()
patchDocument.Replace(s => s.PartOfSpeechId, newPartOfSpeechId);
patchDocument.Replace(s => s.Gloss["en"], "new gloss");

var changes = Sense.ChangesFromJsonPatch(Guid.NewGuid(), patchDocument).ToArray();
var changes = Sense.ChangesFromJsonPatch(_sense, patchDocument).ToArray();

var setPartOfSpeechChange = changes.OfType<SetPartOfSpeechChange>().Should().ContainSingle().Subject;
setPartOfSpeechChange.EntityId.Should().Be(_sense.Id);
setPartOfSpeechChange.PartOfSpeechId.Should().Be(newPartOfSpeechId);

var patchChange = changes.OfType<JsonPatchChange<Sense>>().Should().ContainSingle().Subject;
patchChange.EntityId.Should().Be(_sense.Id);
patchChange.PatchDocument.Operations.Should().ContainSingle().Subject.Value.Should().Be("new gloss");
}

Expand All @@ -30,10 +43,87 @@ public void JsonPatchChangeRewriteDoesNotReturnEmptyPatchChanges()
var patchDocument = new JsonPatchDocument<MiniLcm.Sense>();
patchDocument.Replace(s => s.PartOfSpeechId, newPartOfSpeechId);

var changes = Sense.ChangesFromJsonPatch(Guid.NewGuid(), patchDocument).ToArray();
var changes = Sense.ChangesFromJsonPatch(_sense, patchDocument).ToArray();

var setPartOfSpeechChange = changes.Should().ContainSingle()
.Subject.Should().BeOfType<SetPartOfSpeechChange>().Subject;
setPartOfSpeechChange.EntityId.Should().Be(_sense.Id);
setPartOfSpeechChange.PartOfSpeechId.Should().Be(newPartOfSpeechId);
}

[Fact]
public void RewritesAddSemanticDomainChangesIntoAddSemanticDomainChange()
{
var newSemanticDomainId = Guid.NewGuid();
var patchDocument = new JsonPatchDocument<MiniLcm.Sense>();
patchDocument.Add(s => s.SemanticDomains,
new SemanticDomain() { Id = newSemanticDomainId, Code = "new code", Name = new MultiString() });

var changes = Sense.ChangesFromJsonPatch(_sense, patchDocument).ToArray();

var addSemanticDomainChange = (AddSemanticDomainChange)changes.Should().AllBeOfType<AddSemanticDomainChange>().And.ContainSingle().Subject;
addSemanticDomainChange.EntityId.Should().Be(_sense.Id);
addSemanticDomainChange.SemanticDomain.Id.Should().Be(newSemanticDomainId);
}

[Fact]
public void RewritesReplaceSemanticDomainPatchChangesIntoReplaceSemanticDomainChange()
{
var oldSemanticDomainId = _sense.SemanticDomains[0].Id;
var newSemanticDomainId = Guid.NewGuid();
var patchDocument = new JsonPatchDocument<MiniLcm.Sense>();
patchDocument.Replace(s => s.SemanticDomains, new SemanticDomain() { Id = newSemanticDomainId, Code = "new code", Name = new MultiString() }, 0);

var changes = Sense.ChangesFromJsonPatch(_sense, patchDocument).ToArray();

var replaceSemanticDomainChange = (ReplaceSemanticDomainChange)changes.Should().AllBeOfType<ReplaceSemanticDomainChange>().And.ContainSingle().Subject;
replaceSemanticDomainChange.EntityId.Should().Be(_sense.Id);
replaceSemanticDomainChange.SemanticDomain.Id.Should().Be(newSemanticDomainId);
replaceSemanticDomainChange.OldSemanticDomainId.Should().Be(oldSemanticDomainId);
}
[Fact]
public void RewritesReplaceNoIndexSemanticDomainPatchChangesIntoReplaceSemanticDomainChange()
{
var oldSemanticDomainId = _sense.SemanticDomains[0].Id;
var newSemanticDomainId = Guid.NewGuid();
var patchDocument = new JsonPatchDocument<MiniLcm.Sense>();
patchDocument.Replace(s => s.SemanticDomains, new SemanticDomain() { Id = newSemanticDomainId, Code = "new code", Name = new MultiString() });

var changes = Sense.ChangesFromJsonPatch(_sense, patchDocument).ToArray();

var replaceSemanticDomainChange = (ReplaceSemanticDomainChange)changes.Should().AllBeOfType<ReplaceSemanticDomainChange>().And.ContainSingle().Subject;
replaceSemanticDomainChange.EntityId.Should().Be(_sense.Id);
replaceSemanticDomainChange.SemanticDomain.Id.Should().Be(newSemanticDomainId);
replaceSemanticDomainChange.OldSemanticDomainId.Should().Be(oldSemanticDomainId);
}

[Fact]
public void RewritesRemoveSemanticDomainPatchChangesIntoReplaceSemanticDomainChange()
{
var patchDocument = new JsonPatchDocument<MiniLcm.Sense>();
var semanticDomainIdToRemove = _sense.SemanticDomains[0].Id;
patchDocument.Remove(s => s.SemanticDomains, 0);

var changes = Sense.ChangesFromJsonPatch(_sense, patchDocument).ToArray();

var removeSemanticDomainChange = (RemoveSemanticDomainChange)changes.Should().AllBeOfType<
RemoveSemanticDomainChange>().And.ContainSingle().Subject;
removeSemanticDomainChange.EntityId.Should().Be(_sense.Id);
removeSemanticDomainChange.SemanticDomainId.Should().Be(semanticDomainIdToRemove);
}

[Fact]
public void RewritesRemoveNoIndexSemanticDomainPatchChangesIntoReplaceSemanticDomainChange()
{
var patchDocument = new JsonPatchDocument<MiniLcm.Sense>();
var semanticDomainIdToRemove = _sense.SemanticDomains[0].Id;
patchDocument.Remove(s => s.SemanticDomains);

var changes = Sense.ChangesFromJsonPatch(_sense, patchDocument).ToArray();

var removeSemanticDomainChange = (RemoveSemanticDomainChange)changes.Should().AllBeOfType<
RemoveSemanticDomainChange>().And.ContainSingle().Subject;
removeSemanticDomainChange.EntityId.Should().Be(_sense.Id);
removeSemanticDomainChange.SemanticDomainId.Should().Be(semanticDomainIdToRemove);
}
}
65 changes: 63 additions & 2 deletions backend/LcmCrdt.Tests/LexboxApiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,61 @@ public async Task UpdateSense()
updatedSense.Definition.Values["en"].Should().Be("updated");
}

[Fact]
public async Task CreateSense_WontCreateMissingDomains()
{
var senseId = Guid.NewGuid();
var createdSense = await _api.CreateSense(_entry1Id, new Sense()
{
Id = senseId,
SemanticDomains = [new SemanticDomain() { Id = Guid.NewGuid(), Code = "test", Name = new MultiString() }],
});
createdSense.Id.Should().Be(senseId);
createdSense.SemanticDomains.Should().BeEmpty("because the domain does not exist (or was deleted)");
}


[Fact]
public async Task CreateSense_WillCreateWithExistingDomains()
{
var senseId = Guid.NewGuid();
var semanticDomainId = Guid.NewGuid();
await DataModel.AddChange(Guid.NewGuid(), new CreateSemanticDomainChange(semanticDomainId, new MultiString() { { "en", "test" } }, "test"));
var semanticDomain = await DataModel.GetLatest<Objects.SemanticDomain>(semanticDomainId);
ArgumentNullException.ThrowIfNull(semanticDomain);
var createdSense = await _api.CreateSense(_entry1Id, new Sense()
{
Id = senseId,
SemanticDomains = [semanticDomain],
});
createdSense.Id.Should().Be(senseId);
createdSense.SemanticDomains.Should().ContainSingle(s => s.Id == semanticDomainId);
}

[Fact]
public async Task CreateSense_WontCreateMissingPartOfSpeech()
{
var senseId = Guid.NewGuid();
var createdSense = await _api.CreateSense(_entry1Id,
new Sense() { Id = senseId, PartOfSpeech = "test", PartOfSpeechId = Guid.NewGuid(), });
createdSense.Id.Should().Be(senseId);
createdSense.PartOfSpeechId.Should().BeNull("because the part of speech does not exist (or was deleted)");
}

[Fact]
public async Task CreateSense_WillCreateWthExistingPartOfSpeech()
{
var senseId = Guid.NewGuid();
var partOfSpeechId = Guid.NewGuid();
await DataModel.AddChange(Guid.NewGuid(), new CreatePartOfSpeechChange(partOfSpeechId, new MultiString() { { "en", "test" } }));
var partOfSpeech = await DataModel.GetLatest<Objects.PartOfSpeech>(partOfSpeechId);
ArgumentNullException.ThrowIfNull(partOfSpeech);
var createdSense = await _api.CreateSense(_entry1Id,
new Sense() { Id = senseId, PartOfSpeech = "test", PartOfSpeechId = partOfSpeechId, });
createdSense.Id.Should().Be(senseId);
createdSense.PartOfSpeechId.Should().Be(partOfSpeechId, "because the part of speech does exist");
}

[Fact]
public async Task UpdateSensePartOfSpeech()
{
Expand Down Expand Up @@ -370,6 +425,10 @@ public async Task UpdateSensePartOfSpeech()
[Fact]
public async Task UpdateSenseSemanticDomain()
{
var newDomainId = Guid.NewGuid();
await DataModel.AddChange(Guid.NewGuid(), new CreateSemanticDomainChange(newDomainId, new MultiString() { { "en", "test" } }, "updated"));
var newSemanticDomain = await DataModel.GetLatest<Objects.SemanticDomain>(newDomainId);
ArgumentNullException.ThrowIfNull(newSemanticDomain);
var entry = await _api.CreateEntry(new Entry
{
LexemeForm = new MultiString
Expand Down Expand Up @@ -397,9 +456,11 @@ public async Task UpdateSenseSemanticDomain()
var updatedSense = await _api.UpdateSense(entry.Id,
entry.Senses[0].Id,
_api.CreateUpdateBuilder<Sense>()
.Add(e => e.SemanticDomains, new SemanticDomain() { Id = Guid.Empty, Code = "updated", Name = new MultiString() })
.Add(e => e.SemanticDomains, newSemanticDomain)
.Build());
updatedSense.SemanticDomains.Select(sd => sd.Code).Should().Contain("updated");
var semanticDomain = updatedSense.SemanticDomains.Should().ContainSingle(s => s.Id == newDomainId).Subject;
semanticDomain.Code.Should().Be("updated");
semanticDomain.Id.Should().Be(newDomainId);
}

[Fact]
Expand Down
24 changes: 24 additions & 0 deletions backend/LcmCrdt/Changes/AddSemanticDomainChange.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Crdt.Changes;
using Crdt.Entities;
using MiniLcm;

namespace LcmCrdt.Changes;

public class AddSemanticDomainChange(SemanticDomain semanticDomain, Guid senseId)
: EditChange<Sense>(senseId), ISelfNamedType<AddSemanticDomainChange>
{
public SemanticDomain SemanticDomain { get; } = semanticDomain;

public override async ValueTask ApplyChange(Sense entity, ChangeContext context)
{
if (await context.IsObjectDeleted(SemanticDomain.Id))
{
//do nothing, don't add the domain if it's already deleted
}
else if (entity.SemanticDomains.All(s => s.Id != SemanticDomain.Id))
{
//only add the domain if it's not already in the list
entity.SemanticDomains = [..entity.SemanticDomains, SemanticDomain];
}
}
}
20 changes: 20 additions & 0 deletions backend/LcmCrdt/Changes/CreateSemanticDomainChange.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Crdt;
using Crdt.Changes;
using Crdt.Entities;
using MiniLcm;
using SemanticDomain = LcmCrdt.Objects.SemanticDomain;

namespace LcmCrdt.Changes;

public class CreateSemanticDomainChange(Guid semanticDomainId, MultiString name, string code, bool predefined = false)
: CreateChange<SemanticDomain>(semanticDomainId), ISelfNamedType<CreateSemanticDomainChange>
{
public MultiString Name { get; } = name;
public bool Predefined { get; } = predefined;
public string Code { get; } = code;

public override async ValueTask<IObjectBase> NewEntity(Commit commit, ChangeContext context)
{
return new SemanticDomain { Id = EntityId, Code = Code, Name = Name, Predefined = Predefined };
}
}
3 changes: 3 additions & 0 deletions backend/LcmCrdt/Changes/CreateSenseChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public CreateSenseChange(MiniLcm.Sense sense, Guid entryId) : base(sense.Id == G
SemanticDomains = sense.SemanticDomains;
Gloss = sense.Gloss;
PartOfSpeech = sense.PartOfSpeech;
PartOfSpeechId = sense.PartOfSpeechId;
}

[JsonConstructor]
Expand All @@ -29,6 +30,7 @@ private CreateSenseChange(Guid entityId, Guid entryId) : base(entityId)
public MultiString? Definition { get; set; }
public MultiString? Gloss { get; set; }
public string? PartOfSpeech { get; set; }
public Guid? PartOfSpeechId { get; set; }
public IList<SemanticDomain>? SemanticDomains { get; set; }

public override async ValueTask<IObjectBase> NewEntity(Commit commit, ChangeContext context)
Expand All @@ -40,6 +42,7 @@ public override async ValueTask<IObjectBase> NewEntity(Commit commit, ChangeCont
Definition = Definition ?? new MultiString(),
Gloss = Gloss ?? new MultiString(),
PartOfSpeech = PartOfSpeech ?? string.Empty,
PartOfSpeechId = PartOfSpeechId,
SemanticDomains = SemanticDomains ?? [],
DeletedAt = await context.IsObjectDeleted(EntryId) ? commit.DateTime : (DateTime?)null
};
Expand Down
15 changes: 15 additions & 0 deletions backend/LcmCrdt/Changes/RemoveSemanticDomainChange.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Crdt.Changes;
using Crdt.Entities;

namespace LcmCrdt.Changes;

public class RemoveSemanticDomainChange(Guid semanticDomainId, Guid senseId)
: EditChange<Sense>(senseId), ISelfNamedType<RemoveSemanticDomainChange>
{
public Guid SemanticDomainId { get; } = semanticDomainId;

public override async ValueTask ApplyChange(Sense entity, ChangeContext context)
{
entity.SemanticDomains = [..entity.SemanticDomains.Where(s => s.Id != SemanticDomainId)];
}
}
27 changes: 27 additions & 0 deletions backend/LcmCrdt/Changes/ReplaceSemanticDomainChange.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Crdt.Changes;
using Crdt.Entities;
using MiniLcm;

namespace LcmCrdt.Changes;

public class ReplaceSemanticDomainChange(Guid oldSemanticDomainId, SemanticDomain semanticDomain, Guid senseId)
: EditChange<Sense>(senseId), ISelfNamedType<ReplaceSemanticDomainChange>
{
public Guid OldSemanticDomainId { get; } = oldSemanticDomainId;
public SemanticDomain SemanticDomain { get; } = semanticDomain;

public override async ValueTask ApplyChange(Sense entity, ChangeContext context)
{
//remove the old domain
entity.SemanticDomains = [..entity.SemanticDomains.Where(s => s.Id != OldSemanticDomainId)];
if (await context.IsObjectDeleted(SemanticDomain.Id))
{
//do nothing, don't add the domain if it's already deleted
}
else if (entity.SemanticDomains.All(s => s.Id != SemanticDomain.Id))
{
//only add if it's not already in the list
entity.SemanticDomains = [..entity.SemanticDomains, SemanticDomain];
}
}
}
4 changes: 3 additions & 1 deletion backend/LcmCrdt/CrdtLexboxApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,9 @@ await dataModel.AddChanges(ClientId,
Guid senseId,
UpdateObjectInput<MiniLcm.Sense> update)
{
await dataModel.AddChanges(ClientId, [..Sense.ChangesFromJsonPatch(senseId, update.Patch)]);
var sense = await dataModel.GetLatest<Sense>(senseId);
if (sense is null) throw new NullReferenceException($"unable to find sense with id {senseId}");
await dataModel.AddChanges(ClientId, [..Sense.ChangesFromJsonPatch(sense, update.Patch)]);
return await dataModel.GetLatest<Sense>(senseId) ?? throw new NullReferenceException();
}

Expand Down
Loading

0 comments on commit 192962b

Please sign in to comment.