1package dag_test
2
3import (
4 "encoding/json"
5 "fmt"
6 "os"
7
8 "github.com/MichaelMure/git-bug/entity"
9 "github.com/MichaelMure/git-bug/entity/dag"
10 "github.com/MichaelMure/git-bug/identity"
11 "github.com/MichaelMure/git-bug/repository"
12)
13
14// Note: you can find explanations about the underlying data model here:
15// https://github.com/MichaelMure/git-bug/blob/master/doc/model.md
16
17// This file explains how to define a replicated data structure, stored in and using git as a medium for
18// synchronisation. To do this, we'll use the entity/dag package, which will do all the complex handling.
19//
20// The example we'll use here is a small shared configuration with two fields. One of them is special as
21// it also defines who is allowed to change said configuration.
22// Note: this example is voluntarily a bit complex with operation linking to identities and logic rules,
23// to show that how something more complex than a toy would look like. That said, it's still a simplified
24// example: in git-bug for example, more layers are added for caching, memory handling and to provide an
25// easier to use API.
26//
27// Let's start by defining the document/structure we are going to share:
28
29// Snapshot is the compiled view of a ProjectConfig
30type Snapshot struct {
31 // Administrator is the set of users with the higher level of access
32 Administrator map[identity.Interface]struct{}
33 // SignatureRequired indicate that all git commit need to be signed
34 SignatureRequired bool
35}
36
37// HasAdministrator returns true if the given identity is included in the administrator.
38func (snap *Snapshot) HasAdministrator(i identity.Interface) bool {
39 for admin, _ := range snap.Administrator {
40 if admin.Id() == i.Id() {
41 return true
42 }
43 }
44 return false
45}
46
47// Now, we will not edit this configuration directly. Instead, we are going to apply "operations" on it.
48// Those are the ones that will be stored and shared. Doing things that way allow merging concurrent editing
49// and deal with conflict.
50//
51// Here, we will define three operations:
52// - SetSignatureRequired is a simple operation that set or unset the SignatureRequired boolean
53// - AddAdministrator is more complex and add a new administrator in the Administrator set
54// - RemoveAdministrator is the counterpart the remove administrators
55//
56// Note: there is some amount of boilerplate for operations. In a real project, some of that can be
57// factorized and simplified.
58
59// Operation is the operation interface acting on Snapshot
60type Operation interface {
61 dag.Operation
62
63 // Apply the operation to a Snapshot to create the final state
64 Apply(snapshot *Snapshot)
65}
66
67type OperationType int
68
69const (
70 _ OperationType = iota
71 SetSignatureRequiredOp
72 AddAdministratorOp
73 RemoveAdministratorOp
74)
75
76// SetSignatureRequired is an operation to set/unset if git signature are required.
77type SetSignatureRequired struct {
78 author identity.Interface
79 OperationType OperationType `json:"type"`
80 Value bool `json:"value"`
81}
82
83func NewSetSignatureRequired(author identity.Interface, value bool) *SetSignatureRequired {
84 return &SetSignatureRequired{author: author, OperationType: SetSignatureRequiredOp, Value: value}
85}
86
87func (ssr *SetSignatureRequired) Id() entity.Id {
88 // the Id of the operation is the hash of the serialized data.
89 // we could memorize the Id when deserializing, but that will do
90 data, _ := json.Marshal(ssr)
91 return entity.DeriveId(data)
92}
93
94func (ssr *SetSignatureRequired) Validate() error {
95 if ssr.author == nil {
96 return fmt.Errorf("author not set")
97 }
98 return ssr.author.Validate()
99}
100
101func (ssr *SetSignatureRequired) Author() identity.Interface {
102 return ssr.author
103}
104
105// Apply is the function that makes changes on the snapshot
106func (ssr *SetSignatureRequired) Apply(snapshot *Snapshot) {
107 // check that we are allowed to change the config
108 if _, ok := snapshot.Administrator[ssr.author]; !ok {
109 return
110 }
111 snapshot.SignatureRequired = ssr.Value
112}
113
114// AddAdministrator is an operation to add a new administrator in the set
115type AddAdministrator struct {
116 author identity.Interface
117 OperationType OperationType `json:"type"`
118 ToAdd []identity.Interface `json:"to_add"`
119}
120
121// addAdministratorJson is a helper struct to deserialize identities with a concrete type.
122type addAdministratorJson struct {
123 ToAdd []identity.IdentityStub `json:"to_add"`
124}
125
126func NewAddAdministratorOp(author identity.Interface, toAdd ...identity.Interface) *AddAdministrator {
127 return &AddAdministrator{author: author, OperationType: AddAdministratorOp, ToAdd: toAdd}
128}
129
130func (aa *AddAdministrator) Id() entity.Id {
131 // we could memorize the Id when deserializing, but that will do
132 data, _ := json.Marshal(aa)
133 return entity.DeriveId(data)
134}
135
136func (aa *AddAdministrator) Validate() error {
137 // Let's enforce an arbitrary rule
138 if len(aa.ToAdd) == 0 {
139 return fmt.Errorf("nothing to add")
140 }
141 if aa.author == nil {
142 return fmt.Errorf("author not set")
143 }
144 return aa.author.Validate()
145}
146
147func (aa *AddAdministrator) Author() identity.Interface {
148 return aa.author
149}
150
151// Apply is the function that makes changes on the snapshot
152func (aa *AddAdministrator) Apply(snapshot *Snapshot) {
153 // check that we are allowed to change the config ... or if there is no admin yet
154 if !snapshot.HasAdministrator(aa.author) && len(snapshot.Administrator) != 0 {
155 return
156 }
157 for _, toAdd := range aa.ToAdd {
158 snapshot.Administrator[toAdd] = struct{}{}
159 }
160}
161
162// RemoveAdministrator is an operation to remove an administrator from the set
163type RemoveAdministrator struct {
164 author identity.Interface
165 OperationType OperationType `json:"type"`
166 ToRemove []identity.Interface `json:"to_remove"`
167}
168
169// removeAdministratorJson is a helper struct to deserialize identities with a concrete type.
170type removeAdministratorJson struct {
171 ToRemove []identity.Interface `json:"to_remove"`
172}
173
174func NewRemoveAdministratorOp(author identity.Interface, toRemove ...identity.Interface) *RemoveAdministrator {
175 return &RemoveAdministrator{author: author, OperationType: RemoveAdministratorOp, ToRemove: toRemove}
176}
177
178func (ra *RemoveAdministrator) Id() entity.Id {
179 // the Id of the operation is the hash of the serialized data.
180 // we could memorize the Id when deserializing, but that will do
181 data, _ := json.Marshal(ra)
182 return entity.DeriveId(data)
183}
184
185func (ra *RemoveAdministrator) Validate() error {
186 // Let's enforce some rules. If we return an error, this operation will be
187 // considered invalid and will not be included in our data.
188 if len(ra.ToRemove) == 0 {
189 return fmt.Errorf("nothing to remove")
190 }
191 if ra.author == nil {
192 return fmt.Errorf("author not set")
193 }
194 return ra.author.Validate()
195}
196
197func (ra *RemoveAdministrator) Author() identity.Interface {
198 return ra.author
199}
200
201// Apply is the function that makes changes on the snapshot
202func (ra *RemoveAdministrator) Apply(snapshot *Snapshot) {
203 // check if we are allowed to make changes
204 if !snapshot.HasAdministrator(ra.author) {
205 return
206 }
207 // special rule: we can't end up with no administrator
208 stillSome := false
209 for admin, _ := range snapshot.Administrator {
210 if admin != ra.author {
211 stillSome = true
212 break
213 }
214 }
215 if !stillSome {
216 return
217 }
218 // apply
219 for _, toRemove := range ra.ToRemove {
220 delete(snapshot.Administrator, toRemove)
221 }
222}
223
224// Now, let's create the main object (the entity) we are going to manipulate: ProjectConfig.
225// This object wrap a dag.Entity, which makes it inherit some methods and provide all the complex
226// DAG handling. Additionally, ProjectConfig is the place where we can add functions specific for that type.
227
228type ProjectConfig struct {
229 // this is really all we need
230 *dag.Entity
231}
232
233func NewProjectConfig() *ProjectConfig {
234 return &ProjectConfig{Entity: dag.New(def)}
235}
236
237// a Definition describes a few properties of the Entity, a sort of configuration to manipulate the
238// DAG of operations
239var def = dag.Definition{
240 Typename: "project config",
241 Namespace: "conf",
242 OperationUnmarshaler: operationUnmarshaller,
243 FormatVersion: 1,
244}
245
246// operationUnmarshaller is a function doing the de-serialization of the JSON data into our own
247// concrete Operations. If needed, we can use the resolver to connect to other entities.
248func operationUnmarshaller(author identity.Interface, raw json.RawMessage, resolver identity.Resolver) (dag.Operation, error) {
249 var t struct {
250 OperationType OperationType `json:"type"`
251 }
252
253 if err := json.Unmarshal(raw, &t); err != nil {
254 return nil, err
255 }
256
257 var value interface{}
258
259 switch t.OperationType {
260 case AddAdministratorOp:
261 value = &addAdministratorJson{}
262 case RemoveAdministratorOp:
263 value = &removeAdministratorJson{}
264 case SetSignatureRequiredOp:
265 value = &SetSignatureRequired{}
266 default:
267 panic(fmt.Sprintf("unknown operation type %v", t.OperationType))
268 }
269
270 err := json.Unmarshal(raw, &value)
271 if err != nil {
272 return nil, err
273 }
274
275 var op Operation
276
277 switch value := value.(type) {
278 case *SetSignatureRequired:
279 value.author = author
280 op = value
281 case *addAdministratorJson:
282 // We need something less straightforward to deserialize and resolve identities
283 aa := &AddAdministrator{
284 author: author,
285 OperationType: AddAdministratorOp,
286 ToAdd: make([]identity.Interface, len(value.ToAdd)),
287 }
288 for i, stub := range value.ToAdd {
289 iden, err := resolver.ResolveIdentity(stub.Id())
290 if err != nil {
291 return nil, err
292 }
293 aa.ToAdd[i] = iden
294 }
295 op = aa
296 case *removeAdministratorJson:
297 // We need something less straightforward to deserialize and resolve identities
298 ra := &RemoveAdministrator{
299 author: author,
300 OperationType: RemoveAdministratorOp,
301 ToRemove: make([]identity.Interface, len(value.ToRemove)),
302 }
303 for i, stub := range value.ToRemove {
304 iden, err := resolver.ResolveIdentity(stub.Id())
305 if err != nil {
306 return nil, err
307 }
308 ra.ToRemove[i] = iden
309 }
310 op = ra
311 default:
312 panic(fmt.Sprintf("unknown operation type %T", value))
313 }
314
315 return op, nil
316}
317
318// Compile compute a view of the final state. This is what we would use to display the state
319// in a user interface.
320func (pc ProjectConfig) Compile() *Snapshot {
321 // Note: this would benefit from caching, but it's a simple example
322 snap := &Snapshot{
323 // default value
324 Administrator: make(map[identity.Interface]struct{}),
325 SignatureRequired: false,
326 }
327 for _, op := range pc.Operations() {
328 op.(Operation).Apply(snap)
329 }
330 return snap
331}
332
333// Read is a helper to load a ProjectConfig from a Repository
334func Read(repo repository.ClockedRepo, id entity.Id) (*ProjectConfig, error) {
335 e, err := dag.Read(def, repo, identity.NewSimpleResolver(repo), id)
336 if err != nil {
337 return nil, err
338 }
339 return &ProjectConfig{Entity: e}, nil
340}
341
342func Example_entity() {
343 const gitBugNamespace = "git-bug"
344 // Note: this example ignore errors for readability
345 // Note: variable names get a little confusing as we are simulating both side in the same function
346
347 // Let's start by defining two git repository and connecting them as remote
348 repoRenePath, _ := os.MkdirTemp("", "")
349 repoIsaacPath, _ := os.MkdirTemp("", "")
350 repoRene, _ := repository.InitGoGitRepo(repoRenePath, gitBugNamespace)
351 defer repoRene.Close()
352 repoIsaac, _ := repository.InitGoGitRepo(repoIsaacPath, gitBugNamespace)
353 defer repoIsaac.Close()
354 _ = repoRene.AddRemote("origin", repoIsaacPath)
355 _ = repoIsaac.AddRemote("origin", repoRenePath)
356
357 // Now we need identities and to propagate them
358 rene, _ := identity.NewIdentity(repoRene, "René Descartes", "rene@descartes.fr")
359 isaac, _ := identity.NewIdentity(repoRene, "Isaac Newton", "isaac@newton.uk")
360 _ = rene.Commit(repoRene)
361 _ = isaac.Commit(repoRene)
362 _ = identity.Pull(repoIsaac, "origin")
363
364 // create a new entity
365 confRene := NewProjectConfig()
366
367 // add some operations
368 confRene.Append(NewAddAdministratorOp(rene, rene))
369 confRene.Append(NewAddAdministratorOp(rene, isaac))
370 confRene.Append(NewSetSignatureRequired(rene, true))
371
372 // Rene commits on its own repo
373 _ = confRene.Commit(repoRene)
374
375 // Isaac pull and read the config
376 _ = dag.Pull(def, repoIsaac, identity.NewSimpleResolver(repoIsaac), "origin", isaac)
377 confIsaac, _ := Read(repoIsaac, confRene.Id())
378
379 // Compile gives the current state of the config
380 snapshot := confIsaac.Compile()
381 for admin, _ := range snapshot.Administrator {
382 fmt.Println(admin.DisplayName())
383 }
384
385 // Isaac add more operations
386 confIsaac.Append(NewSetSignatureRequired(isaac, false))
387 reneFromIsaacRepo, _ := identity.ReadLocal(repoIsaac, rene.Id())
388 confIsaac.Append(NewRemoveAdministratorOp(isaac, reneFromIsaacRepo))
389 _ = confIsaac.Commit(repoIsaac)
390}