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