diff --git a/plaintextdocument/document.go b/plaintextdocument/document.go index 3908cb29..3dc693a3 100644 --- a/plaintextdocument/document.go +++ b/plaintextdocument/document.go @@ -4,16 +4,40 @@ import ( "fmt" "github.com/anytypeio/go-anytype-infrastructure-experiments/account" "github.com/anytypeio/go-anytype-infrastructure-experiments/acltree" + "github.com/anytypeio/go-anytype-infrastructure-experiments/testutils/testchanges/pb" "github.com/anytypeio/go-anytype-infrastructure-experiments/thread" + "github.com/gogo/protobuf/proto" ) -type PlainTextDocument struct { +type PlainTextDocument interface { + Text() string + AddText(text string) error +} + +type plainTextDocument struct { heads []string aclTree acltree.ACLTree state *DocumentState } -func (p *PlainTextDocument) Update(tree acltree.ACLTree) { +func (p *plainTextDocument) Text() string { + return p.state.Text +} + +func (p *plainTextDocument) AddText(text string) error { + _, err := p.aclTree.AddContent(func(builder acltree.ChangeBuilder) { + builder.AddChangeContent( + &pb.PlainTextChangeData{ + Content: []*pb.PlainTextChangeContent{ + createAppendTextChangeContent(text), + }, + }) + }) + return err +} + +func (p *plainTextDocument) Update(tree acltree.ACLTree) { + p.aclTree = tree var err error defer func() { if err != nil { @@ -38,7 +62,8 @@ func (p *PlainTextDocument) Update(tree acltree.ACLTree) { }) } -func (p *PlainTextDocument) Rebuild(tree acltree.ACLTree) { +func (p *plainTextDocument) Rebuild(tree acltree.ACLTree) { + p.aclTree = tree p.heads = tree.Heads() var startId string var err error @@ -79,300 +104,54 @@ func (p *PlainTextDocument) Rebuild(tree acltree.ACLTree) { p.state = state } -func NewPlainTextDocument(t thread.Thread, acc *account.AccountData) (*PlainTextDocument, error) { - tree, e +func NewInMemoryPlainTextDocument(acc *account.AccountData, text string) (PlainTextDocument, error) { + return NewPlainTextDocument(acc, thread.NewInMemoryThread, text) } -// -//type AccountData struct { -// Identity string -// SignKey threadmodels.SigningPrivKey -// EncKey threadmodels.EncryptionPrivKey -//} -// -//type Document struct { -// // TODO: ensure that every operation on Document is synchronized -// thread threadmodels.Thread -// stateProvider InitialStateProvider -// accountData *AccountData -// decoder threadmodels.SigningPubKeyDecoder -// -// treeBuilder *acltree.treeBuilder -// aclTreeBuilder *acltree.aclTreeBuilder -// aclStateBuilder *acltree.aclStateBuilder -// snapshotValidator *acltree.snapshotValidator -// docStateBuilder *acltree.documentStateBuilder -// -// docContext *acltree.documentContext -//} -// -//type UpdateResult int -// -//const ( -// UpdateResultNoAction UpdateResult = iota -// UpdateResultAppend -// UpdateResultRebuild -//) -// -//type CreateChangePayload struct { -// ChangesData proto.Marshaler -// ACLData *pb.ACLChangeACLData -// Id string // TODO: this is just for testing, because id should be created automatically from content -//} -// -//func NewDocument( -// thread threadmodels.Thread, -// stateProvider InitialStateProvider, -// accountData *AccountData) *Document { -// -// decoder := threadmodels.NewEd25519Decoder() -// return &Document{ -// thread: thread, -// stateProvider: stateProvider, -// accountData: accountData, -// decoder: decoder, -// aclTreeBuilder: acltree.newACLTreeBuilder(thread, decoder), -// treeBuilder: acltree.newTreeBuilder(thread, decoder), -// snapshotValidator: acltree.newSnapshotValidator(decoder, accountData), -// aclStateBuilder: acltree.newACLStateBuilder(decoder, accountData), -// docStateBuilder: acltree.newDocumentStateBuilder(stateProvider), -// docContext: &acltree.documentContext{}, -// } -//} -// -////sync layer -> Object cache -> document -> Update (..raw changes) -////client layer -> Object cache -> document -> CreateChange(...) -//// -// -//// smartblock -> CreateChange(payload) -//// SmartTree iterate etc -//func (d *Document) CreateChange(payload *CreateChangePayload) error { -// // TODO: add snapshot creation logic -// marshalled, err := payload.ChangesData.Marshal() -// if err != nil { -// return err -// } -// -// encrypted, err := d.docContext.aclState.userReadKeys[d.docContext.aclState.currentReadKeyHash]. -// Encrypt(marshalled) -// if err != nil { -// return err -// } -// -// aclChange := &pb.ACLChange{ -// TreeHeadIds: d.docContext.fullTree.Heads(), -// AclHeadIds: d.getACLHeads(), -// SnapshotBaseId: d.docContext.fullTree.RootId(), -// AclData: payload.ACLData, -// ChangesData: encrypted, -// CurrentReadKeyHash: d.docContext.aclState.currentReadKeyHash, -// Timestamp: 0, -// Identity: d.accountData.Identity, -// } -// -// // TODO: add CID creation logic based on content -// ch := acltree.NewChange(payload.Id, aclChange) -// ch.DecryptedDocumentChange = marshalled -// -// fullMarshalledChange, err := proto.Marshal(aclChange) -// if err != nil { -// return err -// } -// signature, err := d.accountData.SignKey.Sign(fullMarshalledChange) -// if err != nil { -// return err -// } -// -// if aclChange.AclData != nil { -// // we can apply change right away without going through builder, because -// err = d.docContext.aclState.ApplyChange(payload.Id, aclChange) -// if err != nil { -// return err -// } -// } -// d.docContext.fullTree.AddFast(ch) -// -// err = d.thread.AddChange(&thread.RawChange{ -// Payload: marshalled, -// Signature: signature, -// Id: payload.Id, -// }) -// if err != nil { -// return err -// } -// -// d.thread.SetHeads([]string{ch.Id}) -// d.thread.SetMaybeHeads([]string{ch.Id}) -// -// return nil -//} -// -//func (d *Document) Update(changes ...*thread.RawChange) (DocumentState, UpdateResult, error) { -// var treeChanges []*acltree.Change -// -// var foundACLChange bool -// for _, ch := range changes { -// aclChange, err := d.treeBuilder.makeVerifiedACLChange(ch) -// if err != nil { -// return nil, UpdateResultNoAction, fmt.Errorf("change with id %s is incorrect: %w", ch.Id, err) -// } -// -// treeChange := d.treeBuilder.changeCreator(ch.Id, aclChange) -// treeChanges = append(treeChanges, treeChange) -// -// // this already sets PossibleHeads to include new changes -// // TODO: change this behaviour as non-obvious, because it is not evident from the interface -// err = d.thread.AddChange(ch) -// if err != nil { -// return nil, UpdateResultNoAction, fmt.Errorf("change with id %s cannot be added: %w", ch.Id, err) -// } -// if treeChange.IsACLChange() { -// foundACLChange = true -// } -// } -// -// if foundACLChange { -// res, err := d.Build() -// return res, UpdateResultRebuild, err -// } -// -// prevHeads := d.docContext.fullTree.Heads() -// mode := d.docContext.fullTree.Add(treeChanges...) -// switch mode { -// case acltree.Nothing: -// return d.docContext.docState, UpdateResultNoAction, nil -// case acltree.Rebuild: -// res, err := d.Build() -// return res, UpdateResultRebuild, err -// default: -// break -// } -// -// // TODO: we should still check if the user making those changes are able to write using "aclState" -// // decrypting everything, because we have no new keys -// for _, ch := range treeChanges { -// if ch.Content.GetChangesData() != nil { -// key, exists := d.docContext.aclState.userReadKeys[ch.Content.CurrentReadKeyHash] -// if !exists { -// err := fmt.Errorf("failed to find key with hash: %d", ch.Content.CurrentReadKeyHash) -// return nil, UpdateResultNoAction, err -// } -// -// err := ch.DecryptContents(key) -// if err != nil { -// err = fmt.Errorf("failed to decrypt contents for hash: %d", ch.Content.CurrentReadKeyHash) -// return nil, UpdateResultNoAction, err -// } -// } -// } -// -// // because for every new change we know it was after any of the previous heads -// // each of previous heads must have same "Next" nodes -// // so it doesn't matter which one we choose -// // so we choose first one -// newState, err := d.docStateBuilder.appendFrom(prevHeads[0], d.docContext.docState) -// if err != nil { -// res, _ := d.Build() -// return res, UpdateResultRebuild, fmt.Errorf("could not add changes to state, rebuilded") -// } -// -// // setting all heads -// d.thread.SetHeads(d.docContext.fullTree.Heads()) -// // this should be the entrypoint when we build the document -// d.thread.SetMaybeHeads(d.docContext.fullTree.Heads()) -// -// return newState, UpdateResultAppend, nil -//} -// -//func (d *Document) Build() (DocumentState, error) { -// return d.build(false) -//} -// -//// TODO: this should not be the responsibility of Document, move it somewhere else after testing -//func (d *Document) getACLHeads() []string { -// var aclTreeHeads []string -// for _, head := range d.docContext.fullTree.Heads() { -// if slice.FindPos(aclTreeHeads, head) != -1 { // do not scan known heads -// continue -// } -// precedingHeads := d.getPrecedingACLHeads(head) -// -// for _, aclHead := range precedingHeads { -// if slice.FindPos(aclTreeHeads, aclHead) != -1 { -// continue -// } -// aclTreeHeads = append(aclTreeHeads, aclHead) -// } -// } -// return aclTreeHeads -//} -// -//func (d *Document) getPrecedingACLHeads(head string) []string { -// headChange := d.docContext.fullTree.attached[head] -// -// if headChange.Content.GetAclData() != nil { -// return []string{head} -// } else { -// return headChange.Content.AclHeadIds -// } -//} -// -//func (d *Document) build(fromStart bool) (DocumentState, error) { -// d.treeBuilder.init() -// d.aclTreeBuilder.init() -// -// var err error -// d.docContext.fullTree, err = d.treeBuilder.build(fromStart) -// if err != nil { -// return nil, err -// } -// -// // TODO: remove this from context as this is used only to validate snapshot -// d.docContext.aclTree, err = d.aclTreeBuilder.build() -// if err != nil { -// return nil, err -// } -// -// if !fromStart { -// err = d.snapshotValidator.init(d.docContext.aclTree) -// if err != nil { -// return nil, err -// } -// -// valid, err := d.snapshotValidator.validateSnapshot(d.docContext.fullTree.root) -// if err != nil { -// return nil, err -// } -// if !valid { -// return d.build(true) -// } -// } -// err = d.aclStateBuilder.init(d.docContext.fullTree) -// if err != nil { -// return nil, err -// } -// -// d.docContext.aclState, err = d.aclStateBuilder.build() -// if err != nil { -// return nil, err -// } -// -// // tree should be exposed -// -// d.docStateBuilder.init(d.docContext.aclState, d.docContext.fullTree) -// d.docContext.docState, err = d.docStateBuilder.build() -// if err != nil { -// return nil, err -// } -// -// // setting all heads -// d.thread.SetHeads(d.docContext.fullTree.Heads()) -// // this should be the entrypoint when we build the document -// d.thread.SetMaybeHeads(d.docContext.fullTree.Heads()) -// -// return d.docContext.docState, nil -//} -// -//func (d *Document) State() DocumentState { -// return nil -//} +func NewPlainTextDocument( + acc *account.AccountData, + create func(change *thread.RawChange) (thread.Thread, error), + text string) (PlainTextDocument, error) { + changeBuilder := func(builder acltree.ChangeBuilder) { + builder.UserAdd(acc.Identity, acc.EncKey.GetPublic()) + builder.AddChangeContent(createInitialChangeContent(text)) + } + t, err := acltree.BuildThreadWithACL( + acc, + changeBuilder, + create) + if err != nil { + return nil, err + } + + doc := &plainTextDocument{ + heads: nil, + aclTree: nil, + state: nil, + } + tree, err := acltree.BuildACLTree(t, acc, doc) + if err != nil { + return nil, err + } + doc.aclTree = tree + return doc, nil +} + +func createInitialChangeContent(text string) proto.Marshaler { + return &pb.PlainTextChangeData{ + Content: []*pb.PlainTextChangeContent{ + createAppendTextChangeContent(text), + }, + Snapshot: &pb.PlainTextChangeSnapshot{Text: text}, + } +} + +func createAppendTextChangeContent(text string) *pb.PlainTextChangeContent { + return &pb.PlainTextChangeContent{ + Value: &pb.PlainTextChangeContentValueOfTextAppend{ + TextAppend: &pb.PlainTextChangeTextAppend{ + Text: text, + }, + }, + } +} diff --git a/plaintextdocument/document_test.go b/plaintextdocument/document_test.go index a2b6691f..b90f1f9c 100644 --- a/plaintextdocument/document_test.go +++ b/plaintextdocument/document_test.go @@ -1,52 +1,53 @@ package plaintextdocument -import ( - "github.com/anytypeio/go-anytype-infrastructure-experiments/testutils/threadbuilder" - "github.com/stretchr/testify/assert" - "testing" -) - -func TestDocument_Build(t *testing.T) { - thread, err := threadbuilder.NewThreadBuilderFromFile("threadbuilder/userjoinexample.yml") - if err != nil { - t.Fatal(err) - } - keychain := thread.GetKeychain() - accountData := &AccountData{ - Identity: keychain.GetIdentity("A"), - EncKey: keychain.EncryptionKeys["A"], - } - doc := NewDocument(thread, NewPlainTextDocumentStateProvider(), accountData) - res, err := doc.Build() - if err != nil { - t.Fatal(err) - } - - st := res.(*DocumentState) - assert.Equal(t, st.Text, "some text|first") -} - -func TestDocument_Update(t *testing.T) { - thread, err := threadbuilder.NewThreadBuilderFromFile("threadbuilder/userjoinexample.yml") - if err != nil { - t.Fatal(err) - } - keychain := thread.GetKeychain() - accountData := &AccountData{ - Identity: keychain.GetIdentity("A"), - EncKey: keychain.EncryptionKeys["A"], - } - doc := NewDocument(thread, NewPlainTextDocumentStateProvider(), accountData) - res, err := doc.Build() - if err != nil { - t.Fatal(err) - } - - st := res.(*DocumentState) - assert.Equal(t, st.Text, "some text|first") - - rawChs := thread.GetUpdatedChanges() - res, updateResult, err := doc.Update(rawChs...) - assert.Equal(t, updateResult, UpdateResultAppend) - assert.Equal(t, res.(*DocumentState).Text, "some text|first|second") -} +// +//import ( +// "github.com/anytypeio/go-anytype-infrastructure-experiments/testutils/threadbuilder" +// "github.com/stretchr/testify/assert" +// "testing" +//) +// +//func TestDocument_Build(t *testing.T) { +// thread, err := threadbuilder.NewThreadBuilderFromFile("threadbuilder/userjoinexample.yml") +// if err != nil { +// t.Fatal(err) +// } +// keychain := thread.GetKeychain() +// accountData := &AccountData{ +// Identity: keychain.GetIdentity("A"), +// EncKey: keychain.EncryptionKeys["A"], +// } +// doc := NewDocument(thread, NewPlainTextDocumentStateProvider(), accountData) +// res, err := doc.Build() +// if err != nil { +// t.Fatal(err) +// } +// +// st := res.(*DocumentState) +// assert.Equal(t, st.Text, "some text|first") +//} +// +//func TestDocument_Update(t *testing.T) { +// thread, err := threadbuilder.NewThreadBuilderFromFile("threadbuilder/userjoinexample.yml") +// if err != nil { +// t.Fatal(err) +// } +// keychain := thread.GetKeychain() +// accountData := &AccountData{ +// Identity: keychain.GetIdentity("A"), +// EncKey: keychain.EncryptionKeys["A"], +// } +// doc := NewDocument(thread, NewPlainTextDocumentStateProvider(), accountData) +// res, err := doc.Build() +// if err != nil { +// t.Fatal(err) +// } +// +// st := res.(*DocumentState) +// assert.Equal(t, st.Text, "some text|first") +// +// rawChs := thread.GetUpdatedChanges() +// res, updateResult, err := doc.Update(rawChs...) +// assert.Equal(t, updateResult, UpdateResultAppend) +// assert.Equal(t, res.(*DocumentState).Text, "some text|first|second") +//} diff --git a/plaintextdocument/documentcontext.go b/plaintextdocument/documentcontext.go deleted file mode 100644 index 2aae6a97..00000000 --- a/plaintextdocument/documentcontext.go +++ /dev/null @@ -1,10 +0,0 @@ -package plaintextdocument - -import "github.com/anytypeio/go-anytype-infrastructure-experiments/acltree" - -type documentContext struct { - aclTree *acltree.tree // TODO: remove it, because we don't use it - fullTree *acltree.tree - aclState *acltree.aclState - docState DocumentState -} diff --git a/plaintextdocument/documentstatebuilder.go b/plaintextdocument/documentstatebuilder.go deleted file mode 100644 index ab23274e..00000000 --- a/plaintextdocument/documentstatebuilder.go +++ /dev/null @@ -1,87 +0,0 @@ -package plaintextdocument - -import ( - "fmt" - "github.com/anytypeio/go-anytype-infrastructure-experiments/acltree" -) - -// example -> - -type documentStateBuilder struct { - tree *acltree.tree - aclState *acltree.aclState // TODO: decide if this is needed or not - stateProvider InitialStateProvider -} - -func newDocumentStateBuilder(stateProvider InitialStateProvider) *documentStateBuilder { - return &documentStateBuilder{ - stateProvider: stateProvider, - } -} - -func (d *documentStateBuilder) init(aclState *acltree.aclState, tree *acltree.tree) { - d.tree = tree - d.aclState = aclState -} - -// TODO: we should probably merge the two builders into one -func (d *documentStateBuilder) build() (s DocumentState, err error) { - var ( - startId string - count int - ) - rootChange := d.tree.Root() - - if rootChange.DecryptedDocumentChange == nil { - err = fmt.Errorf("root doesn't have decrypted change") - return - } - - s, err = d.stateProvider.ProvideFromInitialChange(rootChange.DecryptedDocumentChange, rootChange.Id) - if err != nil { - return - } - - t := d.tree - startId = rootChange.Id - - t.Iterate(startId, func(c *acltree.Change) (isContinue bool) { - count++ - if startId == c.Id { - return true - } - if c.DecryptedDocumentChange != nil { - s, err = s.ApplyChange(c.DecryptedDocumentChange, c.Id) - if err != nil { - return false - } - } - return true - }) - if err != nil { - return - } - return s, err -} - -func (d *documentStateBuilder) appendFrom(fromId string, init DocumentState) (s DocumentState, err error) { - // TODO: we should do something like state copy probably - s = init - // TODO: we should have the same logic as in aclStateBuilder, that means we should either pass in both methods state from the outside or save the state inside the builder - d.tree.Iterate(fromId, func(c *acltree.Change) (isContinue bool) { - if c.Id == fromId { - return true - } - if c.DecryptedDocumentChange != nil { - s, err = s.ApplyChange(c.DecryptedDocumentChange, c.Id) - if err != nil { - return false - } - } - return true - }) - if err != nil { - return - } - return s, err -} diff --git a/plaintextdocument/documentstatebuilder_test.go b/plaintextdocument/documentstatebuilder_test.go deleted file mode 100644 index b293d212..00000000 --- a/plaintextdocument/documentstatebuilder_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package plaintextdocument - -import ( - "github.com/anytypeio/go-anytype-infrastructure-experiments/acltree" - "github.com/anytypeio/go-anytype-infrastructure-experiments/testutils/threadbuilder" - "github.com/stretchr/testify/assert" - "testing" -) - -func TestDocumentStateBuilder_UserJoinBuild(t *testing.T) { - thread, err := threadbuilder.NewThreadBuilderFromFile("threadbuilder/userjoinexample.yml") - if err != nil { - t.Fatal(err) - } - keychain := thread.GetKeychain() - ctx, err := acltree.createDocumentStateFromThread( - thread, - keychain.GetIdentity("A"), - keychain.EncryptionKeys["A"], - NewPlainTextDocumentStateProvider(), - threadmodels.NewEd25519Decoder()) - if err != nil { - t.Fatalf("should build acl aclState without err: %v", err) - } - - st := ctx.DocState.(*DocumentState) - assert.Equal(t, st.Text, "some text|first") -} - -func TestDocumentStateBuilder_UserRemoveBuild(t *testing.T) { - thread, err := threadbuilder.NewThreadBuilderFromFile("threadbuilder/userremoveexample.yml") - if err != nil { - t.Fatal(err) - } - keychain := thread.GetKeychain() - ctx, err := acltree.createDocumentStateFromThread( - thread, - keychain.GetIdentity("A"), - keychain.EncryptionKeys["A"], - NewPlainTextDocumentStateProvider(), - threadmodels.NewEd25519Decoder()) - if err != nil { - t.Fatalf("should build acl aclState without err: %v", err) - } - - st := ctx.DocState.(*DocumentState) - assert.Equal(t, st.Text, "some text|first") -}