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