From 0ac39a7ab5db077fcf0df827e32bf6e625e980da Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sat, 19 Nov 2022 11:33:12 +0100 Subject: WIP --- entity/dag/interface.go | 6 +++++- entity/dag/operation.go | 7 +++++++ entity/err.go | 17 +++++++++++++++++ entity/interface.go | 12 ++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) (limited to 'entity') diff --git a/entity/dag/interface.go b/entity/dag/interface.go index 613f60e6..80abaced 100644 --- a/entity/dag/interface.go +++ b/entity/dag/interface.go @@ -25,6 +25,10 @@ type Interface[SnapT Snapshot, OpT Operation] interface { // Commit writes the staging area in Git and move the operations to the packs Commit(repo repository.ClockedRepo) error + // CommitAsNeeded execute a Commit only if necessary. This function is useful to avoid getting an error if the Entity + // is already in sync with the repository. + CommitAsNeeded(repo repository.ClockedRepo) error + // FirstOp lookup for the very first operation of the Entity. FirstOp() OpT @@ -32,7 +36,7 @@ type Interface[SnapT Snapshot, OpT Operation] interface { // For a valid Entity, should never be nil LastOp() OpT - // Compile a bug in an easily usable snapshot + // Compile an Entity in an easily usable snapshot Compile() SnapT // CreateLamportTime return the Lamport time of creation diff --git a/entity/dag/operation.go b/entity/dag/operation.go index 1a778878..1b891aeb 100644 --- a/entity/dag/operation.go +++ b/entity/dag/operation.go @@ -63,6 +63,13 @@ type Operation interface { setExtraMetadataImmutable(key string, value string) } +type OperationWithApply[SnapT Snapshot] interface { + Operation + + // Apply the operation to a Snapshot to create the final state + Apply(snapshot SnapT) +} + // OperationWithFiles is an optional extension for an Operation that has files dependency, stored in git. type OperationWithFiles interface { // GetFiles return the files needed by this operation diff --git a/entity/err.go b/entity/err.go index 408e27b4..9d7c266e 100644 --- a/entity/err.go +++ b/entity/err.go @@ -5,6 +5,23 @@ import ( "strings" ) +type ErrNotFound struct { + typename string +} + +func NewErrNotFound(typename string) *ErrNotFound { + return &ErrNotFound{typename: typename} +} + +func (e ErrNotFound) Error() string { + return fmt.Sprintf("%s doesn't exist", e.typename) +} + +func IsErrNotFound(err error) bool { + _, ok := err.(*ErrNotFound) + return ok +} + type ErrMultipleMatch struct { entityType string Matching []Id diff --git a/entity/interface.go b/entity/interface.go index fb4735e4..656d4dc6 100644 --- a/entity/interface.go +++ b/entity/interface.go @@ -10,3 +10,15 @@ type Interface interface { // It is acceptable to use such a hash and keep mutating that data as long as Id() is not called. Id() Id } + +// type Commitable interface { +// Interface +// NeedCommit() bool +// CommitAsNeeded(repo repository.ClockedRepo) error +// Commit(repo repository.ClockedRepo) error +// } + +// +// type Operation interface { +// +// } -- cgit From 4a341b5e1714a6a36ec7f5839a6a1b73571d4851 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Tue, 29 Nov 2022 13:01:53 +0100 Subject: WIP --- entity/dag/common_test.go | 12 +++++++ entity/dag/entity.go | 69 +++++++++++++++++++++++---------------- entity/dag/entity_actions.go | 14 ++++---- entity/dag/entity_actions_test.go | 34 +++++++++---------- entity/dag/entity_test.go | 12 +++---- entity/dag/example_test.go | 14 ++++---- entity/interface.go | 2 ++ 7 files changed, 92 insertions(+), 65 deletions(-) (limited to 'entity') diff --git a/entity/dag/common_test.go b/entity/dag/common_test.go index f78b09e9..51acfa49 100644 --- a/entity/dag/common_test.go +++ b/entity/dag/common_test.go @@ -87,6 +87,18 @@ func unmarshaler(raw json.RawMessage, resolvers entity.Resolvers) (Operation, er return op, nil } +/* + Entity +*/ + +type Foo struct { + *Entity +} + +func wrapper(e *Entity) *Foo { + return &Foo{Entity: e} +} + /* Identities + repo + definition */ diff --git a/entity/dag/entity.go b/entity/dag/entity.go index ca674ad7..03b97aa0 100644 --- a/entity/dag/entity.go +++ b/entity/dag/entity.go @@ -33,6 +33,19 @@ type Definition struct { FormatVersion uint } +type Actions[EntityT entity.Interface] struct { + Wrap func(e *Entity) EntityT + New func() EntityT + Read func(repo repository.ClockedRepo, id entity.Id) (EntityT, error) + ReadWithResolver func(repo repository.ClockedRepo, resolvers entity.Resolvers, id entity.Id) (EntityT, error) + ReadAll func(repo repository.ClockedRepo) <-chan StreamedEntity[EntityT] + ListLocalIds func(repo repository.Repo) ([]entity.Id, error) + Fetch func(repo repository.Repo, remote string) (string, error) + Push func(repo repository.Repo, remote string) (string, error) + Pull func(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) error + MergeAll func(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) <-chan entity.MergeResult +} + // Entity is a data structure stored in a chain of git objects, supporting actions like Push, Pull and Merge. type Entity struct { // A Lamport clock is a logical clock that allow to order event @@ -59,32 +72,32 @@ func New(definition Definition) *Entity { } // Read will read and decode a stored local Entity from a repository -func Read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolvers, id entity.Id) (*Entity, error) { +func Read[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, id entity.Id) (EntityT, error) { if err := id.Validate(); err != nil { - return nil, errors.Wrap(err, "invalid id") + return *new(EntityT), errors.Wrap(err, "invalid id") } ref := fmt.Sprintf("refs/%s/%s", def.Namespace, id.String()) - return read(def, repo, resolvers, ref) + return read[EntityT](def, wrapper, repo, resolvers, ref) } // readRemote will read and decode a stored remote Entity from a repository -func readRemote(def Definition, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, id entity.Id) (*Entity, error) { +func readRemote[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, id entity.Id) (EntityT, error) { if err := id.Validate(); err != nil { - return nil, errors.Wrap(err, "invalid id") + return *new(EntityT), errors.Wrap(err, "invalid id") } ref := fmt.Sprintf("refs/remotes/%s/%s/%s", def.Namespace, remote, id.String()) - return read(def, repo, resolvers, ref) + return read[EntityT](def, wrapper, repo, resolvers, ref) } // read fetch from git and decode an Entity at an arbitrary git reference. -func read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolvers, ref string) (*Entity, error) { +func read[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, ref string) (EntityT, error) { rootHash, err := repo.ResolveRef(ref) if err != nil { - return nil, err + return *new(EntityT), err } // Perform a breadth-first search to get a topological order of the DAG where we discover the @@ -104,7 +117,7 @@ func read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolver commit, err := repo.ReadCommit(hash) if err != nil { - return nil, err + return *new(EntityT), err } BFSOrder = append(BFSOrder, commit) @@ -137,26 +150,26 @@ func read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolver // can have no parents. Said otherwise, the DAG need to have exactly // one leaf. if !isFirstCommit && len(commit.Parents) == 0 { - return nil, fmt.Errorf("multiple leafs in the entity DAG") + return *new(EntityT), fmt.Errorf("multiple leafs in the entity DAG") } opp, err := readOperationPack(def, repo, resolvers, commit) if err != nil { - return nil, err + return *new(EntityT), err } err = opp.Validate() if err != nil { - return nil, err + return *new(EntityT), err } if isMerge && len(opp.Operations) > 0 { - return nil, fmt.Errorf("merge commit cannot have operations") + return *new(EntityT), fmt.Errorf("merge commit cannot have operations") } // Check that the create lamport clock is set (not checked in Validate() as it's optional) if isFirstCommit && opp.CreateTime <= 0 { - return nil, fmt.Errorf("creation lamport time not set") + return *new(EntityT), fmt.Errorf("creation lamport time not set") } // make sure that the lamport clocks causality match the DAG topology @@ -167,7 +180,7 @@ func read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolver } if parentPack.EditTime >= opp.EditTime { - return nil, fmt.Errorf("lamport clock ordering doesn't match the DAG") + return *new(EntityT), fmt.Errorf("lamport clock ordering doesn't match the DAG") } // to avoid an attack where clocks are pushed toward the uint64 rollover, make sure @@ -175,7 +188,7 @@ func read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolver // we ignore merge commits here to allow merging after a loooong time without breaking anything, // as long as there is one valid chain of small hops, it's fine. if !isMerge && opp.EditTime-parentPack.EditTime > 1_000_000 { - return nil, fmt.Errorf("lamport clock jumping too far in the future, likely an attack") + return *new(EntityT), fmt.Errorf("lamport clock jumping too far in the future, likely an attack") } } @@ -187,11 +200,11 @@ func read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolver for _, opp := range oppMap { err = repo.Witness(fmt.Sprintf(creationClockPattern, def.Namespace), opp.CreateTime) if err != nil { - return nil, err + return *new(EntityT), err } err = repo.Witness(fmt.Sprintf(editClockPattern, def.Namespace), opp.EditTime) if err != nil { - return nil, err + return *new(EntityT), err } } @@ -232,13 +245,13 @@ func read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolver } } - return &Entity{ + return wrapper(&Entity{ Definition: def, ops: ops, lastCommit: rootHash, createTime: createTime, editTime: editTime, - }, nil + }), nil } // readClockNoCheck fetch from git, read and witness the clocks of an Entity at an arbitrary git reference. @@ -293,14 +306,14 @@ func readClockNoCheck(def Definition, repo repository.ClockedRepo, ref string) e return nil } -type StreamedEntity struct { - Entity *Entity +type StreamedEntity[EntityT entity.Interface] struct { + Entity EntityT Err error } // ReadAll read and parse all local Entity -func ReadAll(def Definition, repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan StreamedEntity { - out := make(chan StreamedEntity) +func ReadAll[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan StreamedEntity[EntityT] { + out := make(chan StreamedEntity[EntityT]) go func() { defer close(out) @@ -309,19 +322,19 @@ func ReadAll(def Definition, repo repository.ClockedRepo, resolvers entity.Resol refs, err := repo.ListRefs(refPrefix) if err != nil { - out <- StreamedEntity{Err: err} + out <- StreamedEntity[EntityT]{Err: err} return } for _, ref := range refs { - e, err := read(def, repo, resolvers, ref) + e, err := read[EntityT](def, wrapper, repo, resolvers, ref) if err != nil { - out <- StreamedEntity{Err: err} + out <- StreamedEntity[EntityT]{Err: err} return } - out <- StreamedEntity{Entity: e} + out <- StreamedEntity[EntityT]{Entity: e} } }() diff --git a/entity/dag/entity_actions.go b/entity/dag/entity_actions.go index c971f316..2a2bf87f 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, resolvers entity.Resolvers, remote string, author identity.Interface) error { +func Pull[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, 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, resolvers, remote, author) { + for merge := range MergeAll(def, wrapper, repo, resolvers, remote, author) { if merge.Err != nil { return merge.Err } @@ -68,7 +68,7 @@ func Pull(def Definition, repo repository.ClockedRepo, resolvers entity.Resolver // // 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, resolvers entity.Resolvers, remote string, author identity.Interface) <-chan entity.MergeResult { +func MergeAll[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, 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, resolvers entity.Reso } for _, remoteRef := range remoteRefs { - out <- merge(def, repo, resolvers, remoteRef, author) + out <- merge[EntityT](def, wrapper, repo, resolvers, remoteRef, author) } }() @@ -91,14 +91,14 @@ func MergeAll(def Definition, repo repository.ClockedRepo, resolvers entity.Reso // 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, resolvers entity.Resolvers, remoteRef string, author identity.Interface) entity.MergeResult { +func merge[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, 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, resolvers, remoteRef) + remoteEntity, err := read[EntityT](def, wrapper, 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, resolvers entity.Resolve // an empty operationPack. // First step is to collect those clocks. - localEntity, err := read(def, repo, resolvers, localRef) + localEntity, err := read[EntityT](def, wrapper, 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 e6888148..d93059de 100644 --- a/entity/dag/entity_actions_test.go +++ b/entity/dag/entity_actions_test.go @@ -11,10 +11,10 @@ import ( "github.com/MichaelMure/git-bug/repository" ) -func allEntities(t testing.TB, bugs <-chan StreamedEntity) []*Entity { +func allEntities(t testing.TB, bugs <-chan StreamedEntity[*Foo]) []*Foo { t.Helper() - var result []*Entity + var result []*Foo for streamed := range bugs { require.NoError(t, streamed.Err) @@ -36,10 +36,10 @@ func TestEntityPushPull(t *testing.T) { _, err = Push(def, repoA, "remote") require.NoError(t, err) - err = Pull(def, repoB, resolvers, "remote", id1) + err = Pull(def, wrapper, repoB, resolvers, "remote", id1) require.NoError(t, err) - entities := allEntities(t, ReadAll(def, repoB, resolvers)) + entities := allEntities(t, ReadAll(def, wrapper, repoB, resolvers)) require.Len(t, entities, 1) // B --> remote --> A @@ -52,10 +52,10 @@ func TestEntityPushPull(t *testing.T) { _, err = Push(def, repoB, "remote") require.NoError(t, err) - err = Pull(def, repoA, resolvers, "remote", id1) + err = Pull(def, wrapper, repoA, resolvers, "remote", id1) require.NoError(t, err) - entities = allEntities(t, ReadAll(def, repoB, resolvers)) + entities = allEntities(t, ReadAll(def, wrapper, repoB, resolvers)) require.Len(t, entities, 2) } @@ -85,7 +85,7 @@ func TestListLocalIds(t *testing.T) { listLocalIds(t, def, repoA, 2) listLocalIds(t, def, repoB, 0) - err = Pull(def, repoB, resolvers, "remote", id1) + err = Pull(def, wrapper, repoB, resolvers, "remote", id1) require.NoError(t, err) listLocalIds(t, def, repoA, 2) @@ -228,7 +228,7 @@ func TestMerge(t *testing.T) { _, err = Fetch(def, repoB, "remote") require.NoError(t, err) - results := MergeAll(def, repoB, resolvers, "remote", id1) + results := MergeAll(def, wrapper, 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, resolvers, "remote", id1) + results = MergeAll(def, wrapper, 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, resolvers, "remote", id1) + results = MergeAll(def, wrapper, 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, resolvers, "remote", id1) + results = MergeAll(def, wrapper, 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, resolvers, e1A.Id()) + e1B, err := Read(def, wrapper, repoB, resolvers, e1A.Id()) require.NoError(t, err) - e2B, err := Read(def, repoB, resolvers, e2A.Id()) + e2B, err := Read(def, wrapper, 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, resolvers, "remote", id1) + results = MergeAll(def, wrapper, 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, resolvers, "remote", id1) + results = MergeAll(def, wrapper, repoA, resolvers, "remote", id1) assertMergeResults(t, []entity.MergeResult{ { @@ -396,10 +396,10 @@ func TestRemove(t *testing.T) { err = Remove(def, repoA, e.Id()) require.NoError(t, err) - _, err = Read(def, repoA, resolvers, e.Id()) + _, err = Read(def, wrapper, repoA, resolvers, e.Id()) require.Error(t, err) - _, err = readRemote(def, repoA, resolvers, "remote", e.Id()) + _, err = readRemote(def, wrapper, repoA, resolvers, "remote", e.Id()) require.Error(t, err) // Remove is idempotent diff --git a/entity/dag/entity_test.go b/entity/dag/entity_test.go index e399b6c7..c457eb21 100644 --- a/entity/dag/entity_test.go +++ b/entity/dag/entity_test.go @@ -9,7 +9,7 @@ import ( func TestWriteRead(t *testing.T) { repo, id1, id2, resolver, def := makeTestContext() - entity := New(def) + entity := wrapper(New(def)) require.False(t, entity.NeedCommit()) entity.Append(newOp1(id1, "foo")) @@ -24,16 +24,16 @@ func TestWriteRead(t *testing.T) { require.NoError(t, entity.CommitAsNeeded(repo)) require.False(t, entity.NeedCommit()) - read, err := Read(def, repo, resolver, entity.Id()) + read, err := Read(def, wrapper, repo, resolver, entity.Id()) require.NoError(t, err) - assertEqualEntities(t, entity, read) + assertEqualEntities(t, entity.Entity, read.Entity) } func TestWriteReadMultipleAuthor(t *testing.T) { repo, id1, id2, resolver, def := makeTestContext() - entity := New(def) + entity := wrapper(New(def)) entity.Append(newOp1(id1, "foo")) entity.Append(newOp2(id2, "bar")) @@ -43,10 +43,10 @@ func TestWriteReadMultipleAuthor(t *testing.T) { entity.Append(newOp2(id1, "foobar")) require.NoError(t, entity.CommitAsNeeded(repo)) - read, err := Read(def, repo, resolver, entity.Id()) + read, err := Read(def, wrapper, repo, resolver, entity.Id()) require.NoError(t, err) - assertEqualEntities(t, entity, read) + assertEqualEntities(t, entity.Entity, read.Entity) } func assertEqualEntities(t *testing.T, a, b *Entity) { diff --git a/entity/dag/example_test.go b/entity/dag/example_test.go index b1511dc6..a263eb2b 100644 --- a/entity/dag/example_test.go +++ b/entity/dag/example_test.go @@ -200,7 +200,11 @@ type ProjectConfig struct { } func NewProjectConfig() *ProjectConfig { - return &ProjectConfig{Entity: dag.New(def)} + return wrapper(dag.New(def)) +} + +func wrapper(e *dag.Entity) *ProjectConfig { + return &ProjectConfig{Entity: e} } // a Definition describes a few properties of the Entity, a sort of configuration to manipulate the @@ -282,11 +286,7 @@ 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, simpleResolvers(repo), id) - if err != nil { - return nil, err - } - return &ProjectConfig{Entity: e}, nil + return dag.Read(def, wrapper, repo, simpleResolvers(repo), id) } func simpleResolvers(repo repository.ClockedRepo) entity.Resolvers { @@ -331,7 +331,7 @@ func Example_entity() { _ = confRene.Commit(repoRene) // Isaac pull and read the config - _ = dag.Pull(def, repoIsaac, simpleResolvers(repoIsaac), "origin", isaac) + _ = dag.Pull(def, wrapper, repoIsaac, simpleResolvers(repoIsaac), "origin", isaac) confIsaac, _ := Read(repoIsaac, confRene.Id()) // Compile gives the current state of the config diff --git a/entity/interface.go b/entity/interface.go index 656d4dc6..572ba602 100644 --- a/entity/interface.go +++ b/entity/interface.go @@ -9,6 +9,8 @@ type Interface interface { // the root of the entity. // It is acceptable to use such a hash and keep mutating that data as long as Id() is not called. Id() Id + // Validate check if the Entity data is valid + Validate() error } // type Commitable interface { -- cgit From f2def3a9331080a02e57710a859d2aac608ed44c Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Mon, 19 Dec 2022 18:09:59 +0100 Subject: WIP --- entity/resolver.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'entity') diff --git a/entity/resolver.go b/entity/resolver.go index b2f831d7..9cacbf00 100644 --- a/entity/resolver.go +++ b/entity/resolver.go @@ -64,18 +64,18 @@ func (c *CachedResolver) Resolve(id Id) (Interface, error) { return i, nil } -var _ Resolver = ResolverFunc(nil) +var _ Resolver = ResolverFunc[Interface](nil) // ResolverFunc is a helper to morph a function resolver into a Resolver -type ResolverFunc func(id Id) (Interface, error) +type ResolverFunc[T Interface] func(id Id) (T, error) -func (fn ResolverFunc) Resolve(id Id) (Interface, error) { +func (fn ResolverFunc[T]) Resolve(id Id) (Interface, error) { return fn(id) } // MakeResolver create a resolver able to return the given entities. func MakeResolver(entities ...Interface) Resolver { - return ResolverFunc(func(id Id) (Interface, error) { + return ResolverFunc[Interface](func(id Id) (Interface, error) { for _, entity := range entities { if entity.Id() == id { return entity, nil -- cgit From 9b98fc06489353053564b431ac0c0d73a5c55a56 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Wed, 21 Dec 2022 21:54:36 +0100 Subject: cache: tie up the refactor up to compiling --- entity/dag/entity.go | 34 +++++++++++----------------------- entity/dag/entity_actions_test.go | 2 +- entity/err.go | 16 +++++++++++----- entity/interface.go | 12 ------------ entity/resolver.go | 29 ++++++++++++++++++----------- entity/streamed.go | 6 ++++++ 6 files changed, 47 insertions(+), 52 deletions(-) create mode 100644 entity/streamed.go (limited to 'entity') diff --git a/entity/dag/entity.go b/entity/dag/entity.go index 03b97aa0..2028e1b4 100644 --- a/entity/dag/entity.go +++ b/entity/dag/entity.go @@ -33,19 +33,6 @@ type Definition struct { FormatVersion uint } -type Actions[EntityT entity.Interface] struct { - Wrap func(e *Entity) EntityT - New func() EntityT - Read func(repo repository.ClockedRepo, id entity.Id) (EntityT, error) - ReadWithResolver func(repo repository.ClockedRepo, resolvers entity.Resolvers, id entity.Id) (EntityT, error) - ReadAll func(repo repository.ClockedRepo) <-chan StreamedEntity[EntityT] - ListLocalIds func(repo repository.Repo) ([]entity.Id, error) - Fetch func(repo repository.Repo, remote string) (string, error) - Push func(repo repository.Repo, remote string) (string, error) - Pull func(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) error - MergeAll func(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) <-chan entity.MergeResult -} - // Entity is a data structure stored in a chain of git objects, supporting actions like Push, Pull and Merge. type Entity struct { // A Lamport clock is a logical clock that allow to order event @@ -96,6 +83,9 @@ func readRemote[EntityT entity.Interface](def Definition, wrapper func(e *Entity // read fetch from git and decode an Entity at an arbitrary git reference. func read[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, ref string) (EntityT, error) { rootHash, err := repo.ResolveRef(ref) + if err == repository.ErrNotFound { + return *new(EntityT), entity.NewErrNotFound(def.Typename) + } if err != nil { return *new(EntityT), err } @@ -260,6 +250,9 @@ func read[EntityT entity.Interface](def Definition, wrapper func(e *Entity) Enti // operation blobs can be implemented instead. func readClockNoCheck(def Definition, repo repository.ClockedRepo, ref string) error { rootHash, err := repo.ResolveRef(ref) + if err == repository.ErrNotFound { + return entity.NewErrNotFound(def.Typename) + } if err != nil { return err } @@ -306,14 +299,9 @@ func readClockNoCheck(def Definition, repo repository.ClockedRepo, ref string) e return nil } -type StreamedEntity[EntityT entity.Interface] struct { - Entity EntityT - Err error -} - // ReadAll read and parse all local Entity -func ReadAll[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan StreamedEntity[EntityT] { - out := make(chan StreamedEntity[EntityT]) +func ReadAll[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan entity.StreamedEntity[EntityT] { + out := make(chan entity.StreamedEntity[EntityT]) go func() { defer close(out) @@ -322,7 +310,7 @@ func ReadAll[EntityT entity.Interface](def Definition, wrapper func(e *Entity) E refs, err := repo.ListRefs(refPrefix) if err != nil { - out <- StreamedEntity[EntityT]{Err: err} + out <- entity.StreamedEntity[EntityT]{Err: err} return } @@ -330,11 +318,11 @@ func ReadAll[EntityT entity.Interface](def Definition, wrapper func(e *Entity) E e, err := read[EntityT](def, wrapper, repo, resolvers, ref) if err != nil { - out <- StreamedEntity[EntityT]{Err: err} + out <- entity.StreamedEntity[EntityT]{Err: err} return } - out <- StreamedEntity[EntityT]{Entity: e} + out <- entity.StreamedEntity[EntityT]{Entity: e} } }() diff --git a/entity/dag/entity_actions_test.go b/entity/dag/entity_actions_test.go index d93059de..fd219644 100644 --- a/entity/dag/entity_actions_test.go +++ b/entity/dag/entity_actions_test.go @@ -11,7 +11,7 @@ import ( "github.com/MichaelMure/git-bug/repository" ) -func allEntities(t testing.TB, bugs <-chan StreamedEntity[*Foo]) []*Foo { +func allEntities(t testing.TB, bugs <-chan entity.StreamedEntity[*Foo]) []*Foo { t.Helper() var result []*Foo diff --git a/entity/err.go b/entity/err.go index 9d7c266e..4453d36e 100644 --- a/entity/err.go +++ b/entity/err.go @@ -5,6 +5,8 @@ import ( "strings" ) +// ErrNotFound is to be returned when an entity, item, element is +// not found. type ErrNotFound struct { typename string } @@ -22,13 +24,15 @@ func IsErrNotFound(err error) bool { return ok } +// ErrMultipleMatch is to be returned when more than one entity, item, element +// is found, where only one was expected. type ErrMultipleMatch struct { - entityType string - Matching []Id + typename string + Matching []Id } -func NewErrMultipleMatch(entityType string, matching []Id) *ErrMultipleMatch { - return &ErrMultipleMatch{entityType: entityType, Matching: matching} +func NewErrMultipleMatch(typename string, matching []Id) *ErrMultipleMatch { + return &ErrMultipleMatch{typename: typename, Matching: matching} } func (e ErrMultipleMatch) Error() string { @@ -39,7 +43,7 @@ func (e ErrMultipleMatch) Error() string { } return fmt.Sprintf("Multiple matching %s found:\n%s", - e.entityType, + e.typename, strings.Join(matching, "\n")) } @@ -48,6 +52,8 @@ func IsErrMultipleMatch(err error) bool { return ok } +// ErrInvalidFormat is to be returned when reading on-disk data with an unexpected +// format or version. type ErrInvalidFormat struct { version uint expected uint diff --git a/entity/interface.go b/entity/interface.go index 572ba602..3035ac88 100644 --- a/entity/interface.go +++ b/entity/interface.go @@ -12,15 +12,3 @@ type Interface interface { // Validate check if the Entity data is valid Validate() error } - -// type Commitable interface { -// Interface -// NeedCommit() bool -// CommitAsNeeded(repo repository.ClockedRepo) error -// Commit(repo repository.ClockedRepo) error -// } - -// -// type Operation interface { -// -// } diff --git a/entity/resolver.go b/entity/resolver.go index 9cacbf00..bd16b901 100644 --- a/entity/resolver.go +++ b/entity/resolver.go @@ -5,16 +5,23 @@ import ( "sync" ) +// Resolved is a minimal interface on which Resolver operates on. +// Notably, this operates on Entity and Excerpt in the cache. +type Resolved interface { + // Id returns the object identifier. + Id() Id +} + // Resolver is an interface to find an Entity from its Id type Resolver interface { - Resolve(id Id) (Interface, error) + Resolve(id Id) (Resolved, error) } // Resolvers is a collection of Resolver, for different type of Entity -type Resolvers map[Interface]Resolver +type Resolvers map[Resolved]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) { +func Resolve[T Resolved](rs Resolvers, id Id) (T, error) { var zero T for t, resolver := range rs { switch t.(type) { @@ -35,17 +42,17 @@ var _ Resolver = &CachedResolver{} type CachedResolver struct { resolver Resolver mu sync.RWMutex - entities map[Id]Interface + entities map[Id]Resolved } func NewCachedResolver(resolver Resolver) *CachedResolver { return &CachedResolver{ resolver: resolver, - entities: make(map[Id]Interface), + entities: make(map[Id]Resolved), } } -func (c *CachedResolver) Resolve(id Id) (Interface, error) { +func (c *CachedResolver) Resolve(id Id) (Resolved, error) { c.mu.RLock() if i, ok := c.entities[id]; ok { c.mu.RUnlock() @@ -64,18 +71,18 @@ func (c *CachedResolver) Resolve(id Id) (Interface, error) { return i, nil } -var _ Resolver = ResolverFunc[Interface](nil) +var _ Resolver = ResolverFunc[Resolved](nil) // ResolverFunc is a helper to morph a function resolver into a Resolver -type ResolverFunc[T Interface] func(id Id) (T, error) +type ResolverFunc[EntityT Resolved] func(id Id) (EntityT, error) -func (fn ResolverFunc[T]) Resolve(id Id) (Interface, error) { +func (fn ResolverFunc[EntityT]) Resolve(id Id) (Resolved, error) { return fn(id) } // MakeResolver create a resolver able to return the given entities. -func MakeResolver(entities ...Interface) Resolver { - return ResolverFunc[Interface](func(id Id) (Interface, error) { +func MakeResolver(entities ...Resolved) Resolver { + return ResolverFunc[Resolved](func(id Id) (Resolved, error) { for _, entity := range entities { if entity.Id() == id { return entity, nil diff --git a/entity/streamed.go b/entity/streamed.go new file mode 100644 index 00000000..789224a3 --- /dev/null +++ b/entity/streamed.go @@ -0,0 +1,6 @@ +package entity + +type StreamedEntity[EntityT Interface] struct { + Entity EntityT + Err error +} -- cgit From d65e8837aa7bb1a6abb6892b9f2664e1b7edb02e Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Thu, 22 Dec 2022 00:48:00 +0100 Subject: cache: generic withSnapshot, some cleanup --- entity/dag/op_set_metadata_test.go | 6 ++++++ entity/dag/operation.go | 2 ++ 2 files changed, 8 insertions(+) (limited to 'entity') diff --git a/entity/dag/op_set_metadata_test.go b/entity/dag/op_set_metadata_test.go index f4f20e8e..07ece013 100644 --- a/entity/dag/op_set_metadata_test.go +++ b/entity/dag/op_set_metadata_test.go @@ -12,6 +12,8 @@ import ( "github.com/stretchr/testify/require" ) +var _ Snapshot = &snapshotMock{} + type snapshotMock struct { ops []Operation } @@ -20,6 +22,10 @@ func (s *snapshotMock) AllOperations() []Operation { return s.ops } +func (s *snapshotMock) AppendOperation(op Operation) { + s.ops = append(s.ops, op) +} + func TestSetMetadata(t *testing.T) { snap := &snapshotMock{} diff --git a/entity/dag/operation.go b/entity/dag/operation.go index 1b891aeb..f50d91b6 100644 --- a/entity/dag/operation.go +++ b/entity/dag/operation.go @@ -90,6 +90,8 @@ type OperationDoesntChangeSnapshot interface { type Snapshot interface { // AllOperations returns all the operations that have been applied to that snapshot, in order AllOperations() []Operation + // AppendOperation add an operation in the list + AppendOperation(op Operation) } // OpBase implement the common feature that every Operation should support. -- cgit