diff options
author | Michael Muré <batolettre@gmail.com> | 2022-08-13 12:08:48 +0200 |
---|---|---|
committer | Michael Muré <batolettre@gmail.com> | 2022-08-18 15:55:48 +0200 |
commit | 45f5f852b71a63c142bca8b05efe53eebf142594 (patch) | |
tree | cb92d9f598b13dda69fbbc652a21d0ad8dc314c2 /entity | |
parent | cd52872475f1b39f3fb6546606c1e78afb6c08e3 (diff) | |
download | git-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.go | 21 | ||||
-rw-r--r-- | entity/dag/common_test.go | 42 | ||||
-rw-r--r-- | entity/dag/entity.go | 90 | ||||
-rw-r--r-- | entity/dag/entity_actions.go | 14 | ||||
-rw-r--r-- | entity/dag/entity_actions_test.go | 38 | ||||
-rw-r--r-- | entity/dag/example_test.go | 18 | ||||
-rw-r--r-- | entity/dag/operation_pack.go | 42 | ||||
-rw-r--r-- | entity/resolver.go | 74 |
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) +} |