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