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