aboutsummaryrefslogtreecommitdiffstats
path: root/entity
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2022-08-13 12:08:48 +0200
committerMichael Muré <batolettre@gmail.com>2022-08-18 15:55:48 +0200
commit45f5f852b71a63c142bca8b05efe53eebf142594 (patch)
treecb92d9f598b13dda69fbbc652a21d0ad8dc314c2 /entity
parentcd52872475f1b39f3fb6546606c1e78afb6c08e3 (diff)
downloadgit-bug-45f5f852b71a63c142bca8b05efe53eebf142594.tar.gz
core: generalized resolvers to resolve any entity time when unmarshalling an operation
Diffstat (limited to 'entity')
-rw-r--r--entity/dag/clock.go21
-rw-r--r--entity/dag/common_test.go42
-rw-r--r--entity/dag/entity.go90
-rw-r--r--entity/dag/entity_actions.go14
-rw-r--r--entity/dag/entity_actions_test.go38
-rw-r--r--entity/dag/example_test.go18
-rw-r--r--entity/dag/operation_pack.go42
-rw-r--r--entity/resolver.go74
8 files changed, 257 insertions, 82 deletions
diff --git a/entity/dag/clock.go b/entity/dag/clock.go
index 793fa1bf..74a6cd73 100644
--- a/entity/dag/clock.go
+++ b/entity/dag/clock.go
@@ -3,7 +3,8 @@ package dag
import (
"fmt"
- "github.com/MichaelMure/git-bug/identity"
+ "golang.org/x/sync/errgroup"
+
"github.com/MichaelMure/git-bug/repository"
)
@@ -18,21 +19,13 @@ func ClockLoader(defs ...Definition) repository.ClockLoader {
return repository.ClockLoader{
Clocks: clocks,
Witnesser: func(repo repository.ClockedRepo) error {
- // we need to actually load the identities because of the commit signature check when reading,
- // which require the full identities with crypto keys
- resolver := identity.NewCachedResolver(identity.NewSimpleResolver(repo))
-
+ var errG errgroup.Group
for _, def := range defs {
- // we actually just need to read all entities,
- // as that will create and update the clocks
- // TODO: concurrent loading to be faster?
- for b := range ReadAll(def, repo, resolver) {
- if b.Err != nil {
- return b.Err
- }
- }
+ errG.Go(func() error {
+ return ReadAllClocksNoCheck(def, repo)
+ })
}
- return nil
+ return errG.Wait()
},
}
}
diff --git a/entity/dag/common_test.go b/entity/dag/common_test.go
index 774acba8..df8622d4 100644
--- a/entity/dag/common_test.go
+++ b/entity/dag/common_test.go
@@ -59,7 +59,7 @@ func (op *op2) Id() entity.Id {
func (op *op2) Validate() error { return nil }
-func unmarshaler(raw json.RawMessage, resolver identity.Resolver) (Operation, error) {
+func unmarshaler(raw json.RawMessage, resolvers entity.Resolvers) (Operation, error) {
var t struct {
OperationType OperationType `json:"type"`
}
@@ -91,13 +91,13 @@ func unmarshaler(raw json.RawMessage, resolver identity.Resolver) (Operation, er
Identities + repo + definition
*/
-func makeTestContext() (repository.ClockedRepo, identity.Interface, identity.Interface, identity.Resolver, Definition) {
+func makeTestContext() (repository.ClockedRepo, identity.Interface, identity.Interface, entity.Resolvers, Definition) {
repo := repository.NewMockRepo()
- id1, id2, resolver, def := makeTestContextInternal(repo)
- return repo, id1, id2, resolver, def
+ id1, id2, resolvers, def := makeTestContextInternal(repo)
+ return repo, id1, id2, resolvers, def
}
-func makeTestContextRemote(t *testing.T) (repository.ClockedRepo, repository.ClockedRepo, repository.ClockedRepo, identity.Interface, identity.Interface, identity.Resolver, Definition) {
+func makeTestContextRemote(t *testing.T) (repository.ClockedRepo, repository.ClockedRepo, repository.ClockedRepo, identity.Interface, identity.Interface, entity.Resolvers, Definition) {
repoA := repository.CreateGoGitTestRepo(t, false)
repoB := repository.CreateGoGitTestRepo(t, false)
remote := repository.CreateGoGitTestRepo(t, true)
@@ -122,7 +122,7 @@ func makeTestContextRemote(t *testing.T) (repository.ClockedRepo, repository.Clo
return repoA, repoB, remote, id1, id2, resolver, def
}
-func makeTestContextInternal(repo repository.ClockedRepo) (identity.Interface, identity.Interface, identity.Resolver, Definition) {
+func makeTestContextInternal(repo repository.ClockedRepo) (identity.Interface, identity.Interface, entity.Resolvers, Definition) {
id1, err := identity.NewIdentity(repo, "name1", "email1")
if err != nil {
panic(err)
@@ -140,16 +140,18 @@ func makeTestContextInternal(repo repository.ClockedRepo) (identity.Interface, i
panic(err)
}
- resolver := identityResolverFunc(func(id entity.Id) (identity.Interface, error) {
- switch id {
- case id1.Id():
- return id1, nil
- case id2.Id():
- return id2, nil
- default:
- return nil, identity.ErrIdentityNotExist
- }
- })
+ resolvers := entity.Resolvers{
+ &identity.Identity{}: entity.ResolverFunc(func(id entity.Id) (entity.Interface, error) {
+ switch id {
+ case id1.Id():
+ return id1, nil
+ case id2.Id():
+ return id2, nil
+ default:
+ return nil, identity.ErrIdentityNotExist
+ }
+ }),
+ }
def := Definition{
Typename: "foo",
@@ -158,11 +160,5 @@ func makeTestContextInternal(repo repository.ClockedRepo) (identity.Interface, i
FormatVersion: 1,
}
- return id1, id2, resolver, def
-}
-
-type identityResolverFunc func(id entity.Id) (identity.Interface, error)
-
-func (fn identityResolverFunc) ResolveIdentity(id entity.Id) (identity.Interface, error) {
- return fn(id)
+ return id1, id2, resolvers, def
}
diff --git a/entity/dag/entity.go b/entity/dag/entity.go
index 4ccf0e0e..09f37246 100644
--- a/entity/dag/entity.go
+++ b/entity/dag/entity.go
@@ -26,7 +26,7 @@ type Definition struct {
// the Namespace in git references (bugs, prs, ...)
Namespace string
// a function decoding a JSON message into an Operation
- OperationUnmarshaler func(raw json.RawMessage, resolver identity.Resolver) (Operation, error)
+ OperationUnmarshaler func(raw json.RawMessage, resolver entity.Resolvers) (Operation, error)
// the expected format version number, that can be used for data migration/upgrade
FormatVersion uint
}
@@ -57,29 +57,29 @@ func New(definition Definition) *Entity {
}
// Read will read and decode a stored local Entity from a repository
-func Read(def Definition, repo repository.ClockedRepo, resolver identity.Resolver, id entity.Id) (*Entity, error) {
+func Read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolvers, id entity.Id) (*Entity, error) {
if err := id.Validate(); err != nil {
return nil, errors.Wrap(err, "invalid id")
}
ref := fmt.Sprintf("refs/%s/%s", def.Namespace, id.String())
- return read(def, repo, resolver, ref)
+ return read(def, repo, resolvers, ref)
}
// readRemote will read and decode a stored remote Entity from a repository
-func readRemote(def Definition, repo repository.ClockedRepo, resolver identity.Resolver, remote string, id entity.Id) (*Entity, error) {
+func readRemote(def Definition, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, id entity.Id) (*Entity, error) {
if err := id.Validate(); err != nil {
return nil, errors.Wrap(err, "invalid id")
}
ref := fmt.Sprintf("refs/remotes/%s/%s/%s", def.Namespace, remote, id.String())
- return read(def, repo, resolver, ref)
+ return read(def, repo, resolvers, ref)
}
// read fetch from git and decode an Entity at an arbitrary git reference.
-func read(def Definition, repo repository.ClockedRepo, resolver identity.Resolver, ref string) (*Entity, error) {
+func read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolvers, ref string) (*Entity, error) {
rootHash, err := repo.ResolveRef(ref)
if err != nil {
return nil, err
@@ -138,7 +138,7 @@ func read(def Definition, repo repository.ClockedRepo, resolver identity.Resolve
return nil, fmt.Errorf("multiple leafs in the entity DAG")
}
- opp, err := readOperationPack(def, repo, resolver, commit)
+ opp, err := readOperationPack(def, repo, resolvers, commit)
if err != nil {
return nil, err
}
@@ -239,13 +239,65 @@ func read(def Definition, repo repository.ClockedRepo, resolver identity.Resolve
}, nil
}
+// readClockNoCheck fetch from git, read and witness the clocks of an Entity at an arbitrary git reference.
+// Note: readClockNoCheck does not verify the integrity of the Entity and could witness incorrect or incomplete
+// clocks if so. If data integrity check is a requirement, a flow similar to read without actually reading/decoding
+// operation blobs can be implemented instead.
+func readClockNoCheck(def Definition, repo repository.ClockedRepo, ref string) error {
+ rootHash, err := repo.ResolveRef(ref)
+ if err != nil {
+ return err
+ }
+
+ commit, err := repo.ReadCommit(rootHash)
+ if err != nil {
+ return err
+ }
+
+ createTime, editTime, err := readOperationPackClock(repo, commit)
+ if err != nil {
+ return err
+ }
+
+ // if we have more than one commit, we need to find the root to have the create time
+ if len(commit.Parents) > 0 {
+ for len(commit.Parents) > 0 {
+ // The path to the root is irrelevant.
+ commit, err = repo.ReadCommit(commit.Parents[0])
+ if err != nil {
+ return err
+ }
+ }
+ createTime, _, err = readOperationPackClock(repo, commit)
+ if err != nil {
+ return err
+ }
+ }
+
+ if createTime <= 0 {
+ return fmt.Errorf("creation lamport time not set")
+ }
+ if editTime <= 0 {
+ return fmt.Errorf("creation lamport time not set")
+ }
+ err = repo.Witness(fmt.Sprintf(creationClockPattern, def.Namespace), createTime)
+ if err != nil {
+ return err
+ }
+ err = repo.Witness(fmt.Sprintf(editClockPattern, def.Namespace), editTime)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
type StreamedEntity struct {
Entity *Entity
Err error
}
// ReadAll read and parse all local Entity
-func ReadAll(def Definition, repo repository.ClockedRepo, resolver identity.Resolver) <-chan StreamedEntity {
+func ReadAll(def Definition, repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan StreamedEntity {
out := make(chan StreamedEntity)
go func() {
@@ -260,7 +312,7 @@ func ReadAll(def Definition, repo repository.ClockedRepo, resolver identity.Reso
}
for _, ref := range refs {
- e, err := read(def, repo, resolver, ref)
+ e, err := read(def, repo, resolvers, ref)
if err != nil {
out <- StreamedEntity{Err: err}
@@ -274,6 +326,26 @@ func ReadAll(def Definition, repo repository.ClockedRepo, resolver identity.Reso
return out
}
+// ReadAllClocksNoCheck goes over all entities matching Definition and read/witness the corresponding clocks so that the
+// repo end up with correct clocks for the next write.
+func ReadAllClocksNoCheck(def Definition, repo repository.ClockedRepo) error {
+ refPrefix := fmt.Sprintf("refs/%s/", def.Namespace)
+
+ refs, err := repo.ListRefs(refPrefix)
+ if err != nil {
+ return err
+ }
+
+ for _, ref := range refs {
+ err = readClockNoCheck(def, repo, ref)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
// Id return the Entity identifier
func (e *Entity) Id() entity.Id {
// id is the id of the first operation
diff --git a/entity/dag/entity_actions.go b/entity/dag/entity_actions.go
index 5b6e884d..673799ec 100644
--- a/entity/dag/entity_actions.go
+++ b/entity/dag/entity_actions.go
@@ -32,13 +32,13 @@ func Push(def Definition, repo repository.Repo, remote string) (string, error) {
// Pull will do a Fetch + MergeAll
// Contrary to MergeAll, this function will return an error if a merge fail.
-func Pull(def Definition, repo repository.ClockedRepo, resolver identity.Resolver, remote string, author identity.Interface) error {
+func Pull(def Definition, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, author identity.Interface) error {
_, err := Fetch(def, repo, remote)
if err != nil {
return err
}
- for merge := range MergeAll(def, repo, resolver, remote, author) {
+ for merge := range MergeAll(def, repo, resolvers, remote, author) {
if merge.Err != nil {
return merge.Err
}
@@ -68,7 +68,7 @@ func Pull(def Definition, repo repository.ClockedRepo, resolver identity.Resolve
//
// Note: an author is necessary for the case where a merge commit is created, as this commit will
// have an author and may be signed if a signing key is available.
-func MergeAll(def Definition, repo repository.ClockedRepo, resolver identity.Resolver, remote string, author identity.Interface) <-chan entity.MergeResult {
+func MergeAll(def Definition, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, author identity.Interface) <-chan entity.MergeResult {
out := make(chan entity.MergeResult)
go func() {
@@ -82,7 +82,7 @@ func MergeAll(def Definition, repo repository.ClockedRepo, resolver identity.Res
}
for _, remoteRef := range remoteRefs {
- out <- merge(def, repo, resolver, remoteRef, author)
+ out <- merge(def, repo, resolvers, remoteRef, author)
}
}()
@@ -91,14 +91,14 @@ func MergeAll(def Definition, repo repository.ClockedRepo, resolver identity.Res
// merge perform a merge to make sure a local Entity is up-to-date.
// See MergeAll for more details.
-func merge(def Definition, repo repository.ClockedRepo, resolver identity.Resolver, remoteRef string, author identity.Interface) entity.MergeResult {
+func merge(def Definition, repo repository.ClockedRepo, resolvers entity.Resolvers, remoteRef string, author identity.Interface) entity.MergeResult {
id := entity.RefToId(remoteRef)
if err := id.Validate(); err != nil {
return entity.NewMergeInvalidStatus(id, errors.Wrap(err, "invalid ref").Error())
}
- remoteEntity, err := read(def, repo, resolver, remoteRef)
+ remoteEntity, err := read(def, repo, resolvers, remoteRef)
if err != nil {
return entity.NewMergeInvalidStatus(id,
errors.Wrapf(err, "remote %s is not readable", def.Typename).Error())
@@ -197,7 +197,7 @@ func merge(def Definition, repo repository.ClockedRepo, resolver identity.Resolv
// an empty operationPack.
// First step is to collect those clocks.
- localEntity, err := read(def, repo, resolver, localRef)
+ localEntity, err := read(def, repo, resolvers, localRef)
if err != nil {
return entity.NewMergeError(err, id)
}
diff --git a/entity/dag/entity_actions_test.go b/entity/dag/entity_actions_test.go
index 68aa59b8..e6888148 100644
--- a/entity/dag/entity_actions_test.go
+++ b/entity/dag/entity_actions_test.go
@@ -24,7 +24,7 @@ func allEntities(t testing.TB, bugs <-chan StreamedEntity) []*Entity {
}
func TestEntityPushPull(t *testing.T) {
- repoA, repoB, _, id1, id2, resolver, def := makeTestContextRemote(t)
+ repoA, repoB, _, id1, id2, resolvers, def := makeTestContextRemote(t)
// A --> remote --> B
e := New(def)
@@ -36,10 +36,10 @@ func TestEntityPushPull(t *testing.T) {
_, err = Push(def, repoA, "remote")
require.NoError(t, err)
- err = Pull(def, repoB, resolver, "remote", id1)
+ err = Pull(def, repoB, resolvers, "remote", id1)
require.NoError(t, err)
- entities := allEntities(t, ReadAll(def, repoB, resolver))
+ entities := allEntities(t, ReadAll(def, repoB, resolvers))
require.Len(t, entities, 1)
// B --> remote --> A
@@ -52,15 +52,15 @@ func TestEntityPushPull(t *testing.T) {
_, err = Push(def, repoB, "remote")
require.NoError(t, err)
- err = Pull(def, repoA, resolver, "remote", id1)
+ err = Pull(def, repoA, resolvers, "remote", id1)
require.NoError(t, err)
- entities = allEntities(t, ReadAll(def, repoB, resolver))
+ entities = allEntities(t, ReadAll(def, repoB, resolvers))
require.Len(t, entities, 2)
}
func TestListLocalIds(t *testing.T) {
- repoA, repoB, _, id1, id2, resolver, def := makeTestContextRemote(t)
+ repoA, repoB, _, id1, id2, resolvers, def := makeTestContextRemote(t)
// A --> remote --> B
e := New(def)
@@ -85,7 +85,7 @@ func TestListLocalIds(t *testing.T) {
listLocalIds(t, def, repoA, 2)
listLocalIds(t, def, repoB, 0)
- err = Pull(def, repoB, resolver, "remote", id1)
+ err = Pull(def, repoB, resolvers, "remote", id1)
require.NoError(t, err)
listLocalIds(t, def, repoA, 2)
@@ -204,7 +204,7 @@ func assertNotEqualRefs(t *testing.T, repoA, repoB repository.RepoData, prefix s
}
func TestMerge(t *testing.T) {
- repoA, repoB, _, id1, id2, resolver, def := makeTestContextRemote(t)
+ repoA, repoB, _, id1, id2, resolvers, def := makeTestContextRemote(t)
// SCENARIO 1
// if the remote Entity doesn't exist locally, it's created
@@ -228,7 +228,7 @@ func TestMerge(t *testing.T) {
_, err = Fetch(def, repoB, "remote")
require.NoError(t, err)
- results := MergeAll(def, repoB, resolver, "remote", id1)
+ results := MergeAll(def, repoB, resolvers, "remote", id1)
assertMergeResults(t, []entity.MergeResult{
{
@@ -246,7 +246,7 @@ func TestMerge(t *testing.T) {
// SCENARIO 2
// if the remote and local Entity have the same state, nothing is changed
- results = MergeAll(def, repoB, resolver, "remote", id1)
+ results = MergeAll(def, repoB, resolvers, "remote", id1)
assertMergeResults(t, []entity.MergeResult{
{
@@ -272,7 +272,7 @@ func TestMerge(t *testing.T) {
err = e2A.Commit(repoA)
require.NoError(t, err)
- results = MergeAll(def, repoA, resolver, "remote", id1)
+ results = MergeAll(def, repoA, resolvers, "remote", id1)
assertMergeResults(t, []entity.MergeResult{
{
@@ -297,7 +297,7 @@ func TestMerge(t *testing.T) {
_, err = Fetch(def, repoB, "remote")
require.NoError(t, err)
- results = MergeAll(def, repoB, resolver, "remote", id1)
+ results = MergeAll(def, repoB, resolvers, "remote", id1)
assertMergeResults(t, []entity.MergeResult{
{
@@ -324,10 +324,10 @@ func TestMerge(t *testing.T) {
err = e2A.Commit(repoA)
require.NoError(t, err)
- e1B, err := Read(def, repoB, resolver, e1A.Id())
+ e1B, err := Read(def, repoB, resolvers, e1A.Id())
require.NoError(t, err)
- e2B, err := Read(def, repoB, resolver, e2A.Id())
+ e2B, err := Read(def, repoB, resolvers, e2A.Id())
require.NoError(t, err)
e1B.Append(newOp1(id1, "barbarfoofoo"))
@@ -344,7 +344,7 @@ func TestMerge(t *testing.T) {
_, err = Fetch(def, repoB, "remote")
require.NoError(t, err)
- results = MergeAll(def, repoB, resolver, "remote", id1)
+ results = MergeAll(def, repoB, resolvers, "remote", id1)
assertMergeResults(t, []entity.MergeResult{
{
@@ -365,7 +365,7 @@ func TestMerge(t *testing.T) {
_, err = Fetch(def, repoA, "remote")
require.NoError(t, err)
- results = MergeAll(def, repoA, resolver, "remote", id1)
+ results = MergeAll(def, repoA, resolvers, "remote", id1)
assertMergeResults(t, []entity.MergeResult{
{
@@ -384,7 +384,7 @@ func TestMerge(t *testing.T) {
}
func TestRemove(t *testing.T) {
- repoA, _, _, id1, _, resolver, def := makeTestContextRemote(t)
+ repoA, _, _, id1, _, resolvers, def := makeTestContextRemote(t)
e := New(def)
e.Append(newOp1(id1, "foo"))
@@ -396,10 +396,10 @@ func TestRemove(t *testing.T) {
err = Remove(def, repoA, e.Id())
require.NoError(t, err)
- _, err = Read(def, repoA, resolver, e.Id())
+ _, err = Read(def, repoA, resolvers, e.Id())
require.Error(t, err)
- _, err = readRemote(def, repoA, resolver, "remote", e.Id())
+ _, err = readRemote(def, repoA, resolvers, "remote", e.Id())
require.Error(t, err)
// Remove is idempotent
diff --git a/entity/dag/example_test.go b/entity/dag/example_test.go
index 94850bd9..39d77f8d 100644
--- a/entity/dag/example_test.go
+++ b/entity/dag/example_test.go
@@ -214,7 +214,7 @@ var def = dag.Definition{
// operationUnmarshaller is a function doing the de-serialization of the JSON data into our own
// concrete Operations. If needed, we can use the resolver to connect to other entities.
-func operationUnmarshaller(raw json.RawMessage, resolver identity.Resolver) (dag.Operation, error) {
+func operationUnmarshaller(raw json.RawMessage, resolvers entity.Resolvers) (dag.Operation, error) {
var t struct {
OperationType dag.OperationType `json:"type"`
}
@@ -245,7 +245,7 @@ func operationUnmarshaller(raw json.RawMessage, resolver identity.Resolver) (dag
case *AddAdministrator:
// We need to resolve identities
for i, stub := range op.ToAdd {
- iden, err := resolver.ResolveIdentity(stub.Id())
+ iden, err := entity.Resolve[identity.Interface](resolvers, stub.Id())
if err != nil {
return nil, err
}
@@ -254,7 +254,7 @@ func operationUnmarshaller(raw json.RawMessage, resolver identity.Resolver) (dag
case *RemoveAdministrator:
// We need to resolve identities
for i, stub := range op.ToRemove {
- iden, err := resolver.ResolveIdentity(stub.Id())
+ iden, err := entity.Resolve[identity.Interface](resolvers, stub.Id())
if err != nil {
return nil, err
}
@@ -282,13 +282,21 @@ func (pc ProjectConfig) Compile() *Snapshot {
// Read is a helper to load a ProjectConfig from a Repository
func Read(repo repository.ClockedRepo, id entity.Id) (*ProjectConfig, error) {
- e, err := dag.Read(def, repo, identity.NewSimpleResolver(repo), id)
+ e, err := dag.Read(def, repo, simpleResolvers(repo), id)
if err != nil {
return nil, err
}
return &ProjectConfig{Entity: e}, nil
}
+func simpleResolvers(repo repository.ClockedRepo) entity.Resolvers {
+ // resolvers can look a bit complex or out of place here, but it's an important concept
+ // to allow caching and flexibility when constructing the final app.
+ return entity.Resolvers{
+ &identity.Identity{}: identity.NewSimpleResolver(repo),
+ }
+}
+
func Example_entity() {
const gitBugNamespace = "git-bug"
// Note: this example ignore errors for readability
@@ -323,7 +331,7 @@ func Example_entity() {
_ = confRene.Commit(repoRene)
// Isaac pull and read the config
- _ = dag.Pull(def, repoIsaac, identity.NewSimpleResolver(repoIsaac), "origin", isaac)
+ _ = dag.Pull(def, repoIsaac, simpleResolvers(repoIsaac), "origin", isaac)
confIsaac, _ := Read(repoIsaac, confRene.Id())
// Compile gives the current state of the config
diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go
index b2973343..b32a699f 100644
--- a/entity/dag/operation_pack.go
+++ b/entity/dag/operation_pack.go
@@ -206,7 +206,7 @@ func (opp *operationPack) makeExtraTree() []repository.TreeEntry {
// readOperationPack read the operationPack encoded in git at the given Tree hash.
//
// Validity of the Lamport clocks is left for the caller to decide.
-func readOperationPack(def Definition, repo repository.RepoData, resolver identity.Resolver, commit repository.Commit) (*operationPack, error) {
+func readOperationPack(def Definition, repo repository.RepoData, resolvers entity.Resolvers, commit repository.Commit) (*operationPack, error) {
entries, err := repo.ReadTree(commit.TreeHash)
if err != nil {
return nil, err
@@ -247,7 +247,7 @@ func readOperationPack(def Definition, repo repository.RepoData, resolver identi
if err != nil {
return nil, errors.Wrap(err, "failed to read git blob data")
}
- ops, author, err = unmarshallPack(def, resolver, data)
+ ops, author, err = unmarshallPack(def, resolvers, data)
if err != nil {
return nil, err
}
@@ -288,10 +288,42 @@ func readOperationPack(def Definition, repo repository.RepoData, resolver identi
}, nil
}
+// readOperationPackClock is similar to readOperationPack but only read and decode the Lamport clocks.
+// Validity of those is left for the caller to decide.
+func readOperationPackClock(repo repository.RepoData, commit repository.Commit) (lamport.Time, lamport.Time, error) {
+ entries, err := repo.ReadTree(commit.TreeHash)
+ if err != nil {
+ return 0, 0, err
+ }
+
+ var createTime lamport.Time
+ var editTime lamport.Time
+
+ for _, entry := range entries {
+ switch {
+ case strings.HasPrefix(entry.Name, createClockEntryPrefix):
+ v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, createClockEntryPrefix), 10, 64)
+ if err != nil {
+ return 0, 0, errors.Wrap(err, "can't read creation lamport time")
+ }
+ createTime = lamport.Time(v)
+
+ case strings.HasPrefix(entry.Name, editClockEntryPrefix):
+ v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, editClockEntryPrefix), 10, 64)
+ if err != nil {
+ return 0, 0, errors.Wrap(err, "can't read edit lamport time")
+ }
+ editTime = lamport.Time(v)
+ }
+ }
+
+ return createTime, editTime, nil
+}
+
// unmarshallPack delegate the unmarshalling of the Operation's JSON to the decoding
// function provided by the concrete entity. This gives access to the concrete type of each
// Operation.
-func unmarshallPack(def Definition, resolver identity.Resolver, data []byte) ([]Operation, identity.Interface, error) {
+func unmarshallPack(def Definition, resolvers entity.Resolvers, data []byte) ([]Operation, identity.Interface, error) {
aux := struct {
Author identity.IdentityStub `json:"author"`
Operations []json.RawMessage `json:"ops"`
@@ -305,7 +337,7 @@ func unmarshallPack(def Definition, resolver identity.Resolver, data []byte) ([]
return nil, nil, fmt.Errorf("missing author")
}
- author, err := resolver.ResolveIdentity(aux.Author.Id())
+ author, err := entity.Resolve[identity.Interface](resolvers, aux.Author.Id())
if err != nil {
return nil, nil, err
}
@@ -314,7 +346,7 @@ func unmarshallPack(def Definition, resolver identity.Resolver, data []byte) ([]
for _, raw := range aux.Operations {
// delegate to specialized unmarshal function
- op, err := def.OperationUnmarshaler(raw, resolver)
+ op, err := def.OperationUnmarshaler(raw, resolvers)
if err != nil {
return nil, nil, err
}
diff --git a/entity/resolver.go b/entity/resolver.go
new file mode 100644
index 00000000..d4fe5d3e
--- /dev/null
+++ b/entity/resolver.go
@@ -0,0 +1,74 @@
+package entity
+
+import (
+ "fmt"
+ "sync"
+)
+
+// Resolver is an interface to find an Entity from its Id
+type Resolver interface {
+ Resolve(id Id) (Interface, error)
+}
+
+// Resolvers is a collection of Resolver, for different type of Entity
+type Resolvers map[Interface]Resolver
+
+// Resolve use the appropriate sub-resolver for the given type and find the Entity matching the Id.
+func Resolve[T Interface](rs Resolvers, id Id) (T, error) {
+ var zero T
+ for t, resolver := range rs {
+ switch t.(type) {
+ case T:
+ val, err := resolver.(Resolver).Resolve(id)
+ if err != nil {
+ return zero, err
+ }
+ return val.(T), nil
+ }
+ }
+ return zero, fmt.Errorf("unknown type to resolve")
+}
+
+var _ Resolver = &CachedResolver{}
+
+// CachedResolver is a resolver ensuring that loading is done only once through another Resolver.
+type CachedResolver struct {
+ resolver Resolver
+ mu sync.RWMutex
+ entities map[Id]Interface
+}
+
+func NewCachedResolver(resolver Resolver) *CachedResolver {
+ return &CachedResolver{
+ resolver: resolver,
+ entities: make(map[Id]Interface),
+ }
+}
+
+func (c *CachedResolver) Resolve(id Id) (Interface, error) {
+ c.mu.RLock()
+ if i, ok := c.entities[id]; ok {
+ c.mu.RUnlock()
+ return i, nil
+ }
+ c.mu.RUnlock()
+
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ i, err := c.resolver.Resolve(id)
+ if err != nil {
+ return nil, err
+ }
+ c.entities[id] = i
+ return i, nil
+}
+
+var _ Resolver = ResolverFunc(nil)
+
+// ResolverFunc is a helper to morph a function resolver into a Resolver
+type ResolverFunc func(id Id) (Interface, error)
+
+func (fn ResolverFunc) Resolve(id Id) (Interface, error) {
+ return fn(id)
+}