Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore/introduce-object-adapters #15

Merged
merged 14 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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 = (Word) await DataModel.GetBySnapshotId(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 = (Word) await _client2.DataModel.GetBySnapshotId(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 = (Word) await _client1.DataModel.GetBySnapshotId(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 = (Word) await _client1.DataModel.GetBySnapshotId(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 = (Word) await client.DataModel.GetBySnapshotId(simpleSnapshot.Id);
entity.Should().BeEquivalentTo(serverEntity);
}
}
}
Expand Down
Loading