Skip to content

Commit

Permalink
chore/introduce-object-adapters (#15)
Browse files Browse the repository at this point in the history
* rework how objects are registered to use the default adapter which requires each object to implement IObjectBase

* add overload to Change<T> so NewEntity returns a T
  • Loading branch information
hahn-kev authored Oct 15, 2024
1 parent 0340cd4 commit 4200fa1
Show file tree
Hide file tree
Showing 29 changed files with 523 additions and 120 deletions.
2 changes: 1 addition & 1 deletion src/SIL.Harmony.Sample/Changes/NewDefinitionChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class NewDefinitionChange(Guid entityId) : CreateChange<Definition>(entit
public required double Order { get; set; }
public required Guid WordId { get; init; }

public override async ValueTask<IObjectBase> NewEntity(Commit commit, ChangeContext context)
public override async ValueTask<Definition> NewEntity(Commit commit, ChangeContext context)
{
return new Definition
{
Expand Down
2 changes: 1 addition & 1 deletion src/SIL.Harmony.Sample/Changes/NewExampleChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ private NewExampleChange(Guid entityId) : base(entityId)
{
}

public override async ValueTask<IObjectBase> NewEntity(Commit commit, ChangeContext context)
public override async ValueTask<Example> NewEntity(Commit commit, ChangeContext context)
{
return new Example
{
Expand Down
2 changes: 1 addition & 1 deletion src/SIL.Harmony.Sample/Changes/NewWordChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class NewWordChange(Guid entityId, string text, string? note = null) : Cr
public string Text { get; } = text;
public string? Note { get; } = note;

public override ValueTask<IObjectBase> NewEntity(Commit commit, ChangeContext context)
public override ValueTask<Word> NewEntity(Commit commit, ChangeContext context)
{
return new(new Word { Text = Text, Note = Note, Id = EntityId });
}
Expand Down
2 changes: 1 addition & 1 deletion src/SIL.Harmony.Sample/Changes/SetWordTextChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class SetWordTextChange(Guid entityId, string text) : Change<Word>(entity
{
public string Text { get; } = text;

public override ValueTask<IObjectBase> NewEntity(Commit commit, ChangeContext context)
public override ValueTask<Word> NewEntity(Commit commit, ChangeContext context)
{
return new(new Word()
{
Expand Down
2 changes: 1 addition & 1 deletion src/SIL.Harmony.Sample/CrdtSampleKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public static IServiceCollection AddCrdtDataSample(this IServiceCollection servi
.Add<DeleteChange<Definition>>()
.Add<DeleteChange<Example>>()
;
config.ObjectTypeListBuilder
config.ObjectTypeListBuilder.DefaultAdapter()
.Add<Word>()
.Add<Definition>()
.Add<Example>();
Expand Down
200 changes: 200 additions & 0 deletions src/SIL.Harmony.Tests/Adapter/CustomObjectAdapterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using SIL.Harmony.Adapters;
using SIL.Harmony.Changes;
using SIL.Harmony.Db;
using SIL.Harmony.Entities;
using SIL.Harmony.Linq2db;

namespace SIL.Harmony.Tests.Adapter;

public class CustomObjectAdapterTests
{
public class MyDbContext(DbContextOptions<MyDbContext> options, IOptions<CrdtConfig> crdtConfig)
: DbContext(options), ICrdtDbContext
{
public DbSet<MyClass> MyClasses { get; set; } = null!;

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.UseCrdt(crdtConfig.Value);
}
}

[JsonPolymorphic]
[JsonDerivedType(typeof(MyClass), MyClass.ObjectTypeName)]
[JsonDerivedType(typeof(MyClass2), MyClass2.ObjectTypeName)]
public interface IMyCustomInterface
{
Guid Identifier { get; set; }
long? DeletedTime { get; set; }
string TypeName { get; }
IMyCustomInterface Copy();
}

public class MyClass : IMyCustomInterface
{
public const string ObjectTypeName = "MyClassTypeName";
string IMyCustomInterface.TypeName => ObjectTypeName;

public IMyCustomInterface Copy()
{
return new MyClass
{
Identifier = Identifier,
DeletedTime = DeletedTime,
MyString = MyString
};
}

public Guid Identifier { get; set; }
public long? DeletedTime { get; set; }
public string? MyString { get; set; }
}

public class MyClass2 : IMyCustomInterface
{
public const string ObjectTypeName = "MyClassTypeName2";
string IMyCustomInterface.TypeName => ObjectTypeName;

public IMyCustomInterface Copy()
{
return new MyClass2()
{
Identifier = Identifier,
DeletedTime = DeletedTime,
MyNumber = MyNumber
};
}

public Guid Identifier { get; set; }
public long? DeletedTime { get; set; }
public decimal MyNumber { get; set; }
}

public class MyClassAdapter : ICustomAdapter<MyClassAdapter, IMyCustomInterface>
{
public static string AdapterTypeName => "MyClassAdapter";

public static MyClassAdapter Create(IMyCustomInterface obj)
{
return new MyClassAdapter(obj);
}

public IMyCustomInterface Obj { get; }

[JsonConstructor]
public MyClassAdapter(IMyCustomInterface obj)
{
Obj = obj;
}

[JsonIgnore]
public Guid Id => Obj.Identifier;

[JsonIgnore]
public DateTimeOffset? DeletedAt
{
get => Obj.DeletedTime.HasValue ? DateTimeOffset.FromUnixTimeSeconds(Obj.DeletedTime.Value) : null;
set => Obj.DeletedTime = value?.ToUnixTimeSeconds();
}

public string GetObjectTypeName() => Obj.TypeName;

[JsonIgnore]
public object DbObject => Obj;

public Guid[] GetReferences() => [];

public void RemoveReference(Guid id, Commit commit)
{
}

public IObjectBase Copy() => new MyClassAdapter(Obj.Copy());
}

public class CreateMyClassChange : CreateChange<MyClass>, ISelfNamedType<CreateMyClassChange>
{
private readonly MyClass _entity;

public CreateMyClassChange(MyClass entity) : base(entity.Identifier)
{
_entity = entity;
}

public override ValueTask<MyClass> NewEntity(Commit commit, ChangeContext context)
{
return ValueTask.FromResult(_entity);
}
}

public class CreateMyClass2Change : CreateChange<MyClass2>, ISelfNamedType<CreateMyClass2Change>
{
private readonly MyClass2 _entity;

public CreateMyClass2Change(MyClass2 entity) : base(entity.Identifier)
{
_entity = entity;
}

public override ValueTask<MyClass2> NewEntity(Commit commit, ChangeContext context)
{
return ValueTask.FromResult(_entity);
}
}

[Fact]
public async Task CanAdaptACustomObject()
{
var services = new ServiceCollection()
.AddDbContext<MyDbContext>(builder => builder.UseSqlite("Data Source=test.db"))
.AddCrdtData<MyDbContext>(config =>
{
config.ChangeTypeListBuilder.Add<CreateMyClassChange>().Add<CreateMyClass2Change>();
config.ObjectTypeListBuilder
.CustomAdapter<IMyCustomInterface, MyClassAdapter>()
.Add<MyClass>(builder => builder.HasKey(o => o.Identifier))
.Add<MyClass2>(builder => builder.HasKey(o => o.Identifier));
}).BuildServiceProvider();
var myDbContext = services.GetRequiredService<MyDbContext>();
await myDbContext.Database.OpenConnectionAsync();
await myDbContext.Database.EnsureCreatedAsync();
var dataModel = services.GetRequiredService<DataModel>();
var objectId = Guid.NewGuid();
var objectId2 = Guid.NewGuid();
await dataModel.AddChange(Guid.NewGuid(),
new CreateMyClassChange(new MyClass
{
Identifier = objectId,
MyString = "Hello"
}));
await dataModel.AddChange(Guid.NewGuid(),
new CreateMyClass2Change(new MyClass2
{
Identifier = objectId2,
MyNumber = 123.45m
}));

var snapshot = await dataModel.GetLatestSnapshotByObjectId(objectId);
snapshot.Should().NotBeNull();
snapshot.Entity.Should().NotBeNull();
var myClass = snapshot.Entity.Is<MyClass>();
myClass.Should().NotBeNull();
myClass.Identifier.Should().Be(objectId);
myClass.MyString.Should().Be("Hello");
myClass.DeletedTime.Should().BeNull();

var snapshot2 = await dataModel.GetLatestSnapshotByObjectId(objectId2);
snapshot2.Should().NotBeNull();
snapshot2.Entity.Should().NotBeNull();
var myClass2 = snapshot2.Entity.Is<MyClass2>();
myClass2.Should().NotBeNull();
myClass2.Identifier.Should().Be(objectId2);
myClass2.MyNumber.Should().Be(123.45m);
myClass2.DeletedTime.Should().BeNull();

dataModel.GetLatestObjects<MyClass>().Should().NotBeEmpty();
}
}
3 changes: 2 additions & 1 deletion src/SIL.Harmony.Tests/DataModelPerformanceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using SIL.Harmony.Changes;
using SIL.Harmony.Core;
using SIL.Harmony.Db;
using SIL.Harmony.Sample.Changes;
using SIL.Harmony.Sample.Models;
using Xunit.Abstractions;

Expand Down Expand Up @@ -100,7 +101,7 @@ internal static async Task BulkInsertChanges(DataModelTestBase dataModelTest, in
var parentHash = (await dataModelTest.WriteNextChange(dataModelTest.SetWord(Guid.NewGuid(), "entity 1"))).Hash;
for (var i = 0; i < count; i++)
{
var change = dataModelTest.SetWord(Guid.NewGuid(), $"entity {i}");
var change = (SetWordTextChange) dataModelTest.SetWord(Guid.NewGuid(), $"entity {i}");
var commitId = Guid.NewGuid();
var commit = new Commit(commitId)
{
Expand Down
2 changes: 1 addition & 1 deletion src/SIL.Harmony.Tests/DefinitionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public async Task CanAddADefinitionToAWord()
await WriteNextChange(NewDefinition(wordId, "a greeting", "verb"));
var snapshot = await DataModel.GetProjectSnapshot();
var definitionSnapshot = snapshot.Snapshots.Values.Single(s => s.IsType<Definition>());
var definition = (Definition)await DataModel.GetBySnapshotId(definitionSnapshot.Id);
var definition = await DataModel.GetBySnapshotId<Definition>(definitionSnapshot.Id);
definition.Text.Should().Be("a greeting");
definition.WordId.Should().Be(wordId);
}
Expand Down
2 changes: 1 addition & 1 deletion src/SIL.Harmony.Tests/ExampleSentenceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public async Task CanAddAnExampleSentenceToAWord()
await WriteNextChange(NewExampleSentence(definitionId, "Hello, world!"));
var snapshot = await DataModel.GetProjectSnapshot();
var exampleSentenceSnapshot = snapshot.Snapshots.Values.Single(s => s.IsType<Example>());
var exampleSentence = (Example)await DataModel.GetBySnapshotId(exampleSentenceSnapshot.Id);
var exampleSentence = await DataModel.GetBySnapshotId<Example>(exampleSentenceSnapshot.Id);
exampleSentence.Text.Should().Be("Hello, world!");
exampleSentence.DefinitionId.Should().Be(definitionId);
}
Expand Down
7 changes: 3 additions & 4 deletions src/SIL.Harmony.Tests/ModelSnapshotTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,15 @@ public async Task ModelSnapshotShowsMultipleChanges()
var secondChange = await WriteNextChange(SetWord(entityId, "second"));
var snapshot = await DataModel.GetProjectSnapshot();
var simpleSnapshot = snapshot.Snapshots.Values.First();
var entity = await DataModel.GetBySnapshotId(simpleSnapshot.Id);
var entry = entity.Is<Word>();
var entry = await DataModel.GetBySnapshotId<Word>(simpleSnapshot.Id);
entry.Text.Should().Be("second");
snapshot.LastChange.Should().Be(secondChange.DateTime);
}

[Theory]
[InlineData(10)]
[InlineData(100)]
[InlineData(1_000)]
// [InlineData(1_000)]
public async Task CanGetSnapshotFromEarlier(int changeCount)
{
var entityId = Guid.NewGuid();
Expand Down Expand Up @@ -81,7 +80,7 @@ await AddCommitsViaSync(Enumerable.Range(0, changeCount)
var computedModelSnapshots = await DataModel.GetSnapshotsAt(latestSnapshot.Commit.DateTime);

var entitySnapshot = computedModelSnapshots.Should().ContainSingle().Subject.Value;
entitySnapshot.Should().BeEquivalentTo(latestSnapshot, options => options.Excluding(snapshot => snapshot.Id).Excluding(snapshot => snapshot.Commit));
entitySnapshot.Should().BeEquivalentTo(latestSnapshot, options => options.Excluding(snapshot => snapshot.Id).Excluding(snapshot => snapshot.Commit).Excluding(s => s.Entity.DbObject));
var latestSnapshotEntry = latestSnapshot.Entity.Is<Word>();
var entitySnapshotEntry = entitySnapshot.Entity.Is<Word>();
entitySnapshotEntry.Text.Should().Be(latestSnapshotEntry.Text);
Expand Down
11 changes: 11 additions & 0 deletions src/SIL.Harmony.Tests/ObjectBaseTestingHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using SIL.Harmony.Entities;

namespace SIL.Harmony.Tests;

public static class ObjectBaseTestingHelpers
{
public static T Is<T>(this IObjectBase obj) where T : class
{
return (T) obj.DbObject;
}
}
14 changes: 5 additions & 9 deletions src/SIL.Harmony.Tests/SyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,9 @@ public async Task CanSyncSimpleChange()
var client1Snapshot = await _client1.DataModel.GetProjectSnapshot();
var client2Snapshot = await _client2.DataModel.GetProjectSnapshot();
client1Snapshot.LastCommitHash.Should().Be(client2Snapshot.LastCommitHash);
var entity = await _client2.DataModel.GetBySnapshotId(client2Snapshot.Snapshots[entity1Id].Id);
var client2Entity1 = entity.Is<Word>();
var client2Entity1 = await _client2.DataModel.GetBySnapshotId<Word>(client2Snapshot.Snapshots[entity1Id].Id);
client2Entity1.Text.Should().Be("entity1");
var entity1 = await _client1.DataModel.GetBySnapshotId(client1Snapshot.Snapshots[entity2Id].Id);
var client1Entity2 = entity1.Is<Word>();
var client1Entity2 = await _client1.DataModel.GetBySnapshotId<Word>(client1Snapshot.Snapshots[entity2Id].Id);
client1Entity2.Text.Should().Be("entity2");
}

Expand Down Expand Up @@ -95,15 +93,13 @@ public async Task SyncMultipleClientChanges(int clientCount)
serverSnapshot.Snapshots.Should().HaveCount(clientCount + 1);
foreach (var entitySnapshot in serverSnapshot.Snapshots.Values)
{
var entity1 = await _client1.DataModel.GetBySnapshotId(entitySnapshot.Id);
var serverEntity = entity1.Is<Word>();
var serverEntity = await _client1.DataModel.GetBySnapshotId<Word>(entitySnapshot.Id);
foreach (var client in clients)
{
var clientSnapshot = await client.DataModel.GetProjectSnapshot();
var simpleSnapshot = clientSnapshot.Snapshots.Should().ContainKey(entitySnapshot.EntityId).WhoseValue;
var entity2 = await client.DataModel.GetBySnapshotId(simpleSnapshot.Id);
var entity = entity2.Is<Word>();
entity.Should().BeEquivalentTo(serverEntity);
var entity = await client.DataModel.GetBySnapshotId<Word>(simpleSnapshot.Id);
entity.Should().BeEquivalentTo(serverEntity);
}
}
}
Expand Down
Loading

0 comments on commit 4200fa1

Please sign in to comment.