operation.go

  1package dag
  2
  3import (
  4	"crypto/rand"
  5	"encoding/json"
  6	"fmt"
  7	"time"
  8
  9	"github.com/pkg/errors"
 10
 11	"github.com/MichaelMure/git-bug/entities/identity"
 12	"github.com/MichaelMure/git-bug/entity"
 13)
 14
 15type Snapshot entity.SnapshotT[Operation]
 16
 17// Operation is an extended interface for an entity.Operation working with the dag package.
 18type Operation interface {
 19	entity.Operation
 20
 21	// setId allow to set the Id, used when unmarshalling only
 22	setId(id entity.Id)
 23	// setAuthor allow to set the author, used when unmarshalling only
 24	setAuthor(author identity.Interface)
 25	// setExtraMetadataImmutable add a metadata not carried by the operation itself on the operation
 26	setExtraMetadataImmutable(key string, value string)
 27}
 28
 29type OperationWithApply[SnapT Snapshot] interface {
 30	Operation
 31
 32	// Apply the operation to a Snapshot to create the final state
 33	Apply(snapshot SnapT)
 34}
 35
 36// OpBase implement the common feature that every Operation should support.
 37type OpBase struct {
 38	// Not serialized. Store the op's id in memory.
 39	id entity.Id
 40	// Not serialized
 41	author identity.Interface
 42
 43	OperationType entity.OperationType `json:"type"`
 44	UnixTime      int64                `json:"timestamp"`
 45
 46	// mandatory random bytes to ensure a better randomness of the data used to later generate the ID
 47	// len(Nonce) should be > 20 and < 64 bytes
 48	// It has no functional purpose and should be ignored.
 49	Nonce []byte `json:"nonce"`
 50
 51	Metadata map[string]string `json:"metadata,omitempty"`
 52	// Not serialized. Store the extra metadata in memory,
 53	// compiled from SetMetadataOperation.
 54	extraMetadata map[string]string
 55}
 56
 57func NewOpBase(opType entity.OperationType, author identity.Interface, unixTime int64) OpBase {
 58	return OpBase{
 59		OperationType: opType,
 60		author:        author,
 61		UnixTime:      unixTime,
 62		Nonce:         makeNonce(20),
 63		id:            entity.UnsetId,
 64	}
 65}
 66
 67func makeNonce(len int) []byte {
 68	result := make([]byte, len)
 69	_, err := rand.Read(result)
 70	if err != nil {
 71		panic(err)
 72	}
 73	return result
 74}
 75
 76func IdOperation(op Operation, base *OpBase) entity.Id {
 77	if base.id == "" {
 78		// something went really wrong
 79		panic("op's id not set")
 80	}
 81	if base.id == entity.UnsetId {
 82		// This means we are trying to get the op's Id *before* it has been stored, for instance when
 83		// adding multiple ops in one go in an OperationPack.
 84		// As the Id is computed based on the actual bytes written on the disk, we are going to predict
 85		// those and then get the Id. This is safe as it will be the exact same code writing on disk later.
 86
 87		data, err := json.Marshal(op)
 88		if err != nil {
 89			panic(err)
 90		}
 91
 92		base.id = entity.DeriveId(data)
 93	}
 94	return base.id
 95}
 96
 97func (base *OpBase) Type() entity.OperationType {
 98	return base.OperationType
 99}
100
101// Time return the time when the operation was added
102func (base *OpBase) Time() time.Time {
103	return time.Unix(base.UnixTime, 0)
104}
105
106// Validate check the OpBase for errors
107func (base *OpBase) Validate(op entity.Operation, opType entity.OperationType) error {
108	if base.OperationType == 0 {
109		return fmt.Errorf("operation type unset")
110	}
111	if base.OperationType != opType {
112		return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType)
113	}
114
115	if op.Time().Unix() == 0 {
116		return fmt.Errorf("time not set")
117	}
118
119	if base.author == nil {
120		return fmt.Errorf("author not set")
121	}
122
123	if err := op.Author().Validate(); err != nil {
124		return errors.Wrap(err, "author")
125	}
126
127	if op, ok := op.(entity.OperationWithFiles); ok {
128		for _, hash := range op.GetFiles() {
129			if !hash.IsValid() {
130				return fmt.Errorf("file with invalid hash %v", hash)
131			}
132		}
133	}
134
135	if len(base.Nonce) > 64 {
136		return fmt.Errorf("nonce is too big")
137	}
138	if len(base.Nonce) < 20 {
139		return fmt.Errorf("nonce is too small")
140	}
141
142	return nil
143}
144
145// IsAuthored is a sign post method for gqlgen
146func (base *OpBase) IsAuthored() {}
147
148// Author return author identity
149func (base *OpBase) Author() identity.Interface {
150	return base.author
151}
152
153// IdIsSet returns true if the id has been set already
154func (base *OpBase) IdIsSet() bool {
155	return base.id != "" && base.id != entity.UnsetId
156}
157
158// SetMetadata store arbitrary metadata about the operation
159func (base *OpBase) SetMetadata(key string, value string) {
160	if base.IdIsSet() {
161		panic("set metadata on an operation with already an Id")
162	}
163
164	if base.Metadata == nil {
165		base.Metadata = make(map[string]string)
166	}
167	base.Metadata[key] = value
168}
169
170// GetMetadata retrieve arbitrary metadata about the operation
171func (base *OpBase) GetMetadata(key string) (string, bool) {
172	val, ok := base.Metadata[key]
173
174	if ok {
175		return val, true
176	}
177
178	// extraMetadata can't replace the original operations value if any
179	val, ok = base.extraMetadata[key]
180
181	return val, ok
182}
183
184// AllMetadata return all metadata for this operation
185func (base *OpBase) AllMetadata() map[string]string {
186	result := make(map[string]string)
187
188	for key, val := range base.extraMetadata {
189		result[key] = val
190	}
191
192	// Original metadata take precedence
193	for key, val := range base.Metadata {
194		result[key] = val
195	}
196
197	return result
198}
199
200// setId allow to set the Id, used when unmarshalling only
201func (base *OpBase) setId(id entity.Id) {
202	if base.id != "" && base.id != entity.UnsetId {
203		panic("trying to set id again")
204	}
205	base.id = id
206}
207
208// setAuthor allow to set the author, used when unmarshalling only
209func (base *OpBase) setAuthor(author identity.Interface) {
210	base.author = author
211}
212
213func (base *OpBase) setExtraMetadataImmutable(key string, value string) {
214	if base.extraMetadata == nil {
215		base.extraMetadata = make(map[string]string)
216	}
217	if _, exist := base.extraMetadata[key]; !exist {
218		base.extraMetadata[key] = value
219	}
220}