example_test.go

  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}