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}