aboutsummaryrefslogblamecommitdiffstats
path: root/entity/dag/example_test.go
blob: b1511dc681fbf554ea398475d3012336729d1175 (plain) (tree)
1
2
3
4
5
6
7
8
9





                       
              
 
                                                          

                                                   


                                                   



                                                                                                        


                                                                                                         




                                                                                                         








































                                                                                                            
       
                                  






                                                                                   

                                 


                                                                                           



                                                                                         



                                                                      
                                                


                                                   
                                                               




                                                            
                                                               






                                                                         

                                                  


                                                                                                      



                                                                                     


                                            

                                                                      






                                                   
                                                         




                                                                                         
                                                                                        








                                                                              
                  



                                                                                                               



                                                                                          



                                                                      
                                              







                                                                                  
                                                            




                                                           
                                                    




                                                              
                                         






























                                                                                                            
                                                   


                                
                                                                                              
                                                                                        
                                                                                                   
                      
                                                             





                                                       
                            


                                
                                        
                                   
                                           
                                    
                                            



                                                                                
                                       



                               



                                                
                                                                                             


                                               
                                          
                 


                                                  
                                                                                             


                                               
                                             
                 





















                                                                                            
                                                                





                                             







                                                                                               
                       
                                         





                                                                                                          
                                                                              
                              
                                                                                
                               





















                                                                                         
                                                                                 













                                                                            
package dag_test

import (
	"encoding/json"
	"fmt"
	"os"
	"time"

	"github.com/MichaelMure/git-bug/entities/identity"
	"github.com/MichaelMure/git-bug/entity"
	"github.com/MichaelMure/git-bug/entity/dag"
	"github.com/MichaelMure/git-bug/repository"
)

// Note: you can find explanations about the underlying data model here:
// https://github.com/MichaelMure/git-bug/blob/master/doc/model.md

// This file explains how to define a replicated data structure, stored in and using git as a medium for
// synchronisation. To do this, we'll use the entity/dag package, which will do all the complex handling.
//
// The example we'll use here is a small shared configuration with two fields. One of them is special as
// it also defines who is allowed to change said configuration.
// Note: this example is voluntarily a bit complex with operation linking to identities and logic rules,
// to show that how something more complex than a toy would look like. That said, it's still a simplified
// example: in git-bug for example, more layers are added for caching, memory handling and to provide an
// easier to use API.
//
// Let's start by defining the document/structure we are going to share:

// Snapshot is the compiled view of a ProjectConfig
type Snapshot struct {
	// Administrator is the set of users with the higher level of access
	Administrator map[identity.Interface]struct{}
	// SignatureRequired indicate that all git commit need to be signed
	SignatureRequired bool
}

// HasAdministrator returns true if the given identity is included in the administrator.
func (snap *Snapshot) HasAdministrator(i identity.Interface) bool {
	for admin, _ := range snap.Administrator {
		if admin.Id() == i.Id() {
			return true
		}
	}
	return false
}

// Now, we will not edit this configuration directly. Instead, we are going to apply "operations" on it.
// Those are the ones that will be stored and shared. Doing things that way allow merging concurrent editing
// and deal with conflict.
//
// Here, we will define three operations:
// - SetSignatureRequired is a simple operation that set or unset the SignatureRequired boolean
// - AddAdministrator is more complex and add a new administrator in the Administrator set
// - RemoveAdministrator is the counterpart the remove administrators
//
// Note: there is some amount of boilerplate for operations. In a real project, some of that can be
// factorized and simplified.

// Operation is the operation interface acting on Snapshot
type Operation interface {
	dag.Operation

	// Apply the operation to a Snapshot to create the final state
	Apply(snapshot *Snapshot)
}

const (
	_ dag.OperationType = iota
	SetSignatureRequiredOp
	AddAdministratorOp
	RemoveAdministratorOp
)

// SetSignatureRequired is an operation to set/unset if git signature are required.
type SetSignatureRequired struct {
	dag.OpBase
	Value bool `json:"value"`
}

func NewSetSignatureRequired(author identity.Interface, value bool) *SetSignatureRequired {
	return &SetSignatureRequired{
		OpBase: dag.NewOpBase(SetSignatureRequiredOp, author, time.Now().Unix()),
		Value:  value,
	}
}

func (ssr *SetSignatureRequired) Id() entity.Id {
	// the Id of the operation is the hash of the serialized data.
	return dag.IdOperation(ssr, &ssr.OpBase)
}

func (ssr *SetSignatureRequired) Validate() error {
	return ssr.OpBase.Validate(ssr, SetSignatureRequiredOp)
}

// Apply is the function that makes changes on the snapshot
func (ssr *SetSignatureRequired) Apply(snapshot *Snapshot) {
	// check that we are allowed to change the config
	if _, ok := snapshot.Administrator[ssr.Author()]; !ok {
		return
	}
	snapshot.SignatureRequired = ssr.Value
}

// AddAdministrator is an operation to add a new administrator in the set
type AddAdministrator struct {
	dag.OpBase
	ToAdd []identity.Interface `json:"to_add"`
}

func NewAddAdministratorOp(author identity.Interface, toAdd ...identity.Interface) *AddAdministrator {
	return &AddAdministrator{
		OpBase: dag.NewOpBase(AddAdministratorOp, author, time.Now().Unix()),
		ToAdd:  toAdd,
	}
}

func (aa *AddAdministrator) Id() entity.Id {
	// the Id of the operation is the hash of the serialized data.
	return dag.IdOperation(aa, &aa.OpBase)
}

func (aa *AddAdministrator) Validate() error {
	// Let's enforce an arbitrary rule
	if len(aa.ToAdd) == 0 {
		return fmt.Errorf("nothing to add")
	}
	return aa.OpBase.Validate(aa, AddAdministratorOp)
}

// Apply is the function that makes changes on the snapshot
func (aa *AddAdministrator) Apply(snapshot *Snapshot) {
	// check that we are allowed to change the config ... or if there is no admin yet
	if !snapshot.HasAdministrator(aa.Author()) && len(snapshot.Administrator) != 0 {
		return
	}
	for _, toAdd := range aa.ToAdd {
		snapshot.Administrator[toAdd] = struct{}{}
	}
}

// RemoveAdministrator is an operation to remove an administrator from the set
type RemoveAdministrator struct {
	dag.OpBase
	ToRemove []identity.Interface `json:"to_remove"`
}

func NewRemoveAdministratorOp(author identity.Interface, toRemove ...identity.Interface) *RemoveAdministrator {
	return &RemoveAdministrator{
		OpBase:   dag.NewOpBase(RemoveAdministratorOp, author, time.Now().Unix()),
		ToRemove: toRemove,
	}
}

func (ra *RemoveAdministrator) Id() entity.Id {
	// the Id of the operation is the hash of the serialized data.
	return dag.IdOperation(ra, &ra.OpBase)
}

func (ra *RemoveAdministrator) Validate() error {
	// Let's enforce some rules. If we return an error, this operation will be
	// considered invalid and will not be included in our data.
	if len(ra.ToRemove) == 0 {
		return fmt.Errorf("nothing to remove")
	}
	return ra.OpBase.Validate(ra, RemoveAdministratorOp)
}

// Apply is the function that makes changes on the snapshot
func (ra *RemoveAdministrator) Apply(snapshot *Snapshot) {
	// check if we are allowed to make changes
	if !snapshot.HasAdministrator(ra.Author()) {
		return
	}
	// special rule: we can't end up with no administrator
	stillSome := false
	for admin, _ := range snapshot.Administrator {
		if admin != ra.Author() {
			stillSome = true
			break
		}
	}
	if !stillSome {
		return
	}
	// apply
	for _, toRemove := range ra.ToRemove {
		delete(snapshot.Administrator, toRemove)
	}
}

// Now, let's create the main object (the entity) we are going to manipulate: ProjectConfig.
// This object wrap a dag.Entity, which makes it inherit some methods and provide all the complex
// DAG handling. Additionally, ProjectConfig is the place where we can add functions specific for that type.

type ProjectConfig struct {
	// this is really all we need
	*dag.Entity
}

func NewProjectConfig() *ProjectConfig {
	return &ProjectConfig{Entity: dag.New(def)}
}

// a Definition describes a few properties of the Entity, a sort of configuration to manipulate the
// DAG of operations
var def = dag.Definition{
	Typename:             "project config",
	Namespace:            "conf",
	OperationUnmarshaler: operationUnmarshaler,
	FormatVersion:        1,
}

// operationUnmarshaler is a function doing the de-serialization of the JSON data into our own
// concrete Operations. If needed, we can use the resolver to connect to other entities.
func operationUnmarshaler(raw json.RawMessage, resolvers entity.Resolvers) (dag.Operation, error) {
	var t struct {
		OperationType dag.OperationType `json:"type"`
	}

	if err := json.Unmarshal(raw, &t); err != nil {
		return nil, err
	}

	var op dag.Operation

	switch t.OperationType {
	case AddAdministratorOp:
		op = &AddAdministrator{}
	case RemoveAdministratorOp:
		op = &RemoveAdministrator{}
	case SetSignatureRequiredOp:
		op = &SetSignatureRequired{}
	default:
		panic(fmt.Sprintf("unknown operation type %v", t.OperationType))
	}

	err := json.Unmarshal(raw, &op)
	if err != nil {
		return nil, err
	}

	switch op := op.(type) {
	case *AddAdministrator:
		// We need to resolve identities
		for i, stub := range op.ToAdd {
			iden, err := entity.Resolve[identity.Interface](resolvers, stub.Id())
			if err != nil {
				return nil, err
			}
			op.ToAdd[i] = iden
		}
	case *RemoveAdministrator:
		// We need to resolve identities
		for i, stub := range op.ToRemove {
			iden, err := entity.Resolve[identity.Interface](resolvers, stub.Id())
			if err != nil {
				return nil, err
			}
			op.ToRemove[i] = iden
		}
	}

	return op, nil
}

// Compile compute a view of the final state. This is what we would use to display the state
// in a user interface.
func (pc ProjectConfig) Compile() *Snapshot {
	// Note: this would benefit from caching, but it's a simple example
	snap := &Snapshot{
		// default value
		Administrator:     make(map[identity.Interface]struct{}),
		SignatureRequired: false,
	}
	for _, op := range pc.Operations() {
		op.(Operation).Apply(snap)
	}
	return snap
}

// Read is a helper to load a ProjectConfig from a Repository
func Read(repo repository.ClockedRepo, id entity.Id) (*ProjectConfig, error) {
	e, err := dag.Read(def, repo, simpleResolvers(repo), id)
	if err != nil {
		return nil, err
	}
	return &ProjectConfig{Entity: e}, nil
}

func simpleResolvers(repo repository.ClockedRepo) entity.Resolvers {
	// resolvers can look a bit complex or out of place here, but it's an important concept
	// to allow caching and flexibility when constructing the final app.
	return entity.Resolvers{
		&identity.Identity{}: identity.NewSimpleResolver(repo),
	}
}

func Example_entity() {
	const gitBugNamespace = "git-bug"
	// Note: this example ignore errors for readability
	// Note: variable names get a little confusing as we are simulating both side in the same function

	// Let's start by defining two git repository and connecting them as remote
	repoRenePath, _ := os.MkdirTemp("", "")
	repoIsaacPath, _ := os.MkdirTemp("", "")
	repoRene, _ := repository.InitGoGitRepo(repoRenePath, gitBugNamespace)
	defer repoRene.Close()
	repoIsaac, _ := repository.InitGoGitRepo(repoIsaacPath, gitBugNamespace)
	defer repoIsaac.Close()
	_ = repoRene.AddRemote("origin", repoIsaacPath)
	_ = repoIsaac.AddRemote("origin", repoRenePath)

	// Now we need identities and to propagate them
	rene, _ := identity.NewIdentity(repoRene, "René Descartes", "rene@descartes.fr")
	isaac, _ := identity.NewIdentity(repoRene, "Isaac Newton", "isaac@newton.uk")
	_ = rene.Commit(repoRene)
	_ = isaac.Commit(repoRene)
	_ = identity.Pull(repoIsaac, "origin")

	// create a new entity
	confRene := NewProjectConfig()

	// add some operations
	confRene.Append(NewAddAdministratorOp(rene, rene))
	confRene.Append(NewAddAdministratorOp(rene, isaac))
	confRene.Append(NewSetSignatureRequired(rene, true))

	// Rene commits on its own repo
	_ = confRene.Commit(repoRene)

	// Isaac pull and read the config
	_ = dag.Pull(def, repoIsaac, simpleResolvers(repoIsaac), "origin", isaac)
	confIsaac, _ := Read(repoIsaac, confRene.Id())

	// Compile gives the current state of the config
	snapshot := confIsaac.Compile()
	for admin, _ := range snapshot.Administrator {
		fmt.Println(admin.DisplayName())
	}

	// Isaac add more operations
	confIsaac.Append(NewSetSignatureRequired(isaac, false))
	reneFromIsaacRepo, _ := identity.ReadLocal(repoIsaac, rene.Id())
	confIsaac.Append(NewRemoveAdministratorOp(isaac, reneFromIsaacRepo))
	_ = confIsaac.Commit(repoIsaac)
}