diff options
Diffstat (limited to 'entity/dag/operation.go')
-rw-r--r-- | entity/dag/operation.go | 232 |
1 files changed, 228 insertions, 4 deletions
diff --git a/entity/dag/operation.go b/entity/dag/operation.go index a320859f..0227b3e0 100644 --- a/entity/dag/operation.go +++ b/entity/dag/operation.go @@ -1,11 +1,21 @@ package dag import ( + "crypto/rand" + "encoding/json" + "fmt" + "time" + + "github.com/pkg/errors" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" ) +// OperationType is an operation type identifier +type OperationType int + // Operation is a piece of data defining a change to reflect on the state of an Entity. // What this Operation or Entity's state looks like is not of the resort of this package as it only deals with the // data structure and storage. @@ -22,23 +32,39 @@ type Operation interface { // a minimal amount of entropy and avoid collision. // // Author's note: I tried to find a clever way around that inelegance (stuffing random useless data into the stored - // structure is not exactly elegant) but I failed to find a proper way. Essentially, anything that would reuse some + // structure is not exactly elegant), but I failed to find a proper way. Essentially, anything that would reuse some // other data (parent operation's Id, lamport clock) or the graph structure (depth) impose that the Id would only // make sense in the context of the graph and yield some deep coupling between Entity and Operation. This in turn // make the whole thing even less elegant. // // A common way to derive an Id will be to use the entity.DeriveId() function on the serialized operation data. Id() entity.Id + // Type return the type of the operation + Type() OperationType // Validate check if the Operation data is valid Validate() error // Author returns the author of this operation Author() identity.Interface + // Time return the time when the operation was added + Time() time.Time + + // SetMetadata store arbitrary metadata about the operation + SetMetadata(key string, value string) + // GetMetadata retrieve arbitrary metadata about the operation + GetMetadata(key string) (string, bool) + // AllMetadata return all metadata for this operation + AllMetadata() map[string]string + + // setId allow to set the Id, used when unmarshalling only + setId(id entity.Id) + // setAuthor allow to set the author, used when unmarshalling only + setAuthor(author identity.Interface) + // setExtraMetadataImmutable add a metadata not carried by the operation itself on the operation + setExtraMetadataImmutable(key string, value string) } -// OperationWithFiles is an extended Operation that has files dependency, stored in git. +// OperationWithFiles is an optional extension for an Operation that has files dependency, stored in git. type OperationWithFiles interface { - Operation - // GetFiles return the files needed by this operation // This implies that the Operation maintain and store internally the references to those files. This is how // this information is read later, when loading from storage. @@ -46,3 +72,201 @@ type OperationWithFiles interface { // hash). GetFiles() []repository.Hash } + +// OperationDoesntChangeSnapshot is an interface signaling that the Operation implementing it doesn't change the +// snapshot, for example a metadata operation that act on other operations. +type OperationDoesntChangeSnapshot interface { + DoesntChangeSnapshot() +} + +// Snapshot is the minimal interface that a snapshot need to implement +type Snapshot interface { + // AllOperations returns all the operations that have been applied to that snapshot, in order + AllOperations() []Operation +} + +// OpBase implement the common feature that every Operation should support. +type OpBase struct { + // Not serialized. Store the op's id in memory. + id entity.Id + // Not serialized + author identity.Interface + + OperationType OperationType `json:"type"` + UnixTime int64 `json:"timestamp"` + + // mandatory random bytes to ensure a better randomness of the data used to later generate the ID + // len(Nonce) should be > 20 and < 64 bytes + // It has no functional purpose and should be ignored. + Nonce []byte `json:"nonce"` + + Metadata map[string]string `json:"metadata,omitempty"` + // Not serialized. Store the extra metadata in memory, + // compiled from SetMetadataOperation. + extraMetadata map[string]string +} + +func NewOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase { + return OpBase{ + OperationType: opType, + author: author, + UnixTime: unixTime, + Nonce: makeNonce(20), + id: entity.UnsetId, + } +} + +func makeNonce(len int) []byte { + result := make([]byte, len) + _, err := rand.Read(result) + if err != nil { + panic(err) + } + return result +} + +func IdOperation(op Operation, base *OpBase) entity.Id { + if base.id == "" { + // something went really wrong + panic("op's id not set") + } + if base.id == entity.UnsetId { + // This means we are trying to get the op's Id *before* it has been stored, for instance when + // adding multiple ops in one go in an OperationPack. + // As the Id is computed based on the actual bytes written on the disk, we are going to predict + // those and then get the Id. This is safe as it will be the exact same code writing on disk later. + + data, err := json.Marshal(op) + if err != nil { + panic(err) + } + + base.id = entity.DeriveId(data) + } + return base.id +} + +func (base *OpBase) Type() OperationType { + return base.OperationType +} + +// Time return the time when the operation was added +func (base *OpBase) Time() time.Time { + return time.Unix(base.UnixTime, 0) +} + +// Validate check the OpBase for errors +func (base *OpBase) Validate(op Operation, opType OperationType) error { + if base.OperationType == 0 { + return fmt.Errorf("operation type unset") + } + if base.OperationType != opType { + return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType) + } + + if op.Time().Unix() == 0 { + return fmt.Errorf("time not set") + } + + if base.author == nil { + return fmt.Errorf("author not set") + } + + if err := op.Author().Validate(); err != nil { + return errors.Wrap(err, "author") + } + + if op, ok := op.(OperationWithFiles); ok { + for _, hash := range op.GetFiles() { + if !hash.IsValid() { + return fmt.Errorf("file with invalid hash %v", hash) + } + } + } + + if len(base.Nonce) > 64 { + return fmt.Errorf("nonce is too big") + } + if len(base.Nonce) < 20 { + return fmt.Errorf("nonce is too small") + } + + return nil +} + +// IsAuthored is a sign post method for gqlgen +func (base *OpBase) IsAuthored() {} + +// Author return author identity +func (base *OpBase) Author() identity.Interface { + return base.author +} + +// IdIsSet returns true if the id has been set already +func (base *OpBase) IdIsSet() bool { + return base.id != "" && base.id != entity.UnsetId +} + +// SetMetadata store arbitrary metadata about the operation +func (base *OpBase) SetMetadata(key string, value string) { + if base.IdIsSet() { + panic("set metadata on an operation with already an Id") + } + + if base.Metadata == nil { + base.Metadata = make(map[string]string) + } + base.Metadata[key] = value +} + +// GetMetadata retrieve arbitrary metadata about the operation +func (base *OpBase) GetMetadata(key string) (string, bool) { + val, ok := base.Metadata[key] + + if ok { + return val, true + } + + // extraMetadata can't replace the original operations value if any + val, ok = base.extraMetadata[key] + + return val, ok +} + +// AllMetadata return all metadata for this operation +func (base *OpBase) AllMetadata() map[string]string { + result := make(map[string]string) + + for key, val := range base.extraMetadata { + result[key] = val + } + + // Original metadata take precedence + for key, val := range base.Metadata { + result[key] = val + } + + return result +} + +// setId allow to set the Id, used when unmarshalling only +func (base *OpBase) setId(id entity.Id) { + if base.id != "" && base.id != entity.UnsetId { + panic("trying to set id again") + } + base.id = id +} + +// setAuthor allow to set the author, used when unmarshalling only +func (base *OpBase) setAuthor(author identity.Interface) { + base.author = author +} + +func (base *OpBase) setExtraMetadataImmutable(key string, value string) { + if base.extraMetadata == nil { + base.extraMetadata = make(map[string]string) + } + if _, exist := base.extraMetadata[key]; !exist { + base.extraMetadata[key] = value + } +} |