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}