WIP plain text document

This commit is contained in:
mcrakhman 2022-07-12 08:45:13 +02:00 committed by Mikhail Iudin
parent 0a118c4891
commit 02d2b948a3
No known key found for this signature in database
GPG Key ID: FAAAA8BAABDFF1C0
5 changed files with 128 additions and 493 deletions

View File

@ -4,16 +4,40 @@ import (
"fmt" "fmt"
"github.com/anytypeio/go-anytype-infrastructure-experiments/account" "github.com/anytypeio/go-anytype-infrastructure-experiments/account"
"github.com/anytypeio/go-anytype-infrastructure-experiments/acltree" "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/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 heads []string
aclTree acltree.ACLTree aclTree acltree.ACLTree
state *DocumentState 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 var err error
defer func() { defer func() {
if err != nil { 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() p.heads = tree.Heads()
var startId string var startId string
var err error var err error
@ -79,300 +104,54 @@ func (p *PlainTextDocument) Rebuild(tree acltree.ACLTree) {
p.state = state p.state = state
} }
func NewPlainTextDocument(t thread.Thread, acc *account.AccountData) (*PlainTextDocument, error) { func NewInMemoryPlainTextDocument(acc *account.AccountData, text string) (PlainTextDocument, error) {
tree, e return NewPlainTextDocument(acc, thread.NewInMemoryThread, text)
} }
// func NewPlainTextDocument(
//type AccountData struct { acc *account.AccountData,
// Identity string create func(change *thread.RawChange) (thread.Thread, error),
// SignKey threadmodels.SigningPrivKey text string) (PlainTextDocument, error) {
// EncKey threadmodels.EncryptionPrivKey changeBuilder := func(builder acltree.ChangeBuilder) {
//} builder.UserAdd(acc.Identity, acc.EncKey.GetPublic())
// builder.AddChangeContent(createInitialChangeContent(text))
//type Document struct { }
// // TODO: ensure that every operation on Document is synchronized t, err := acltree.BuildThreadWithACL(
// thread threadmodels.Thread acc,
// stateProvider InitialStateProvider changeBuilder,
// accountData *AccountData create)
// decoder threadmodels.SigningPubKeyDecoder if err != nil {
// return nil, err
// treeBuilder *acltree.treeBuilder }
// aclTreeBuilder *acltree.aclTreeBuilder
// aclStateBuilder *acltree.aclStateBuilder doc := &plainTextDocument{
// snapshotValidator *acltree.snapshotValidator heads: nil,
// docStateBuilder *acltree.documentStateBuilder aclTree: nil,
// state: nil,
// docContext *acltree.documentContext }
//} tree, err := acltree.BuildACLTree(t, acc, doc)
// if err != nil {
//type UpdateResult int return nil, err
// }
//const ( doc.aclTree = tree
// UpdateResultNoAction UpdateResult = iota return doc, nil
// UpdateResultAppend }
// UpdateResultRebuild
//) func createInitialChangeContent(text string) proto.Marshaler {
// return &pb.PlainTextChangeData{
//type CreateChangePayload struct { Content: []*pb.PlainTextChangeContent{
// ChangesData proto.Marshaler createAppendTextChangeContent(text),
// ACLData *pb.ACLChangeACLData },
// Id string // TODO: this is just for testing, because id should be created automatically from content Snapshot: &pb.PlainTextChangeSnapshot{Text: text},
//} }
// }
//func NewDocument(
// thread threadmodels.Thread, func createAppendTextChangeContent(text string) *pb.PlainTextChangeContent {
// stateProvider InitialStateProvider, return &pb.PlainTextChangeContent{
// accountData *AccountData) *Document { Value: &pb.PlainTextChangeContentValueOfTextAppend{
// TextAppend: &pb.PlainTextChangeTextAppend{
// decoder := threadmodels.NewEd25519Decoder() Text: text,
// 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
//}

View File

@ -1,52 +1,53 @@
package plaintextdocument package plaintextdocument
import ( //
"github.com/anytypeio/go-anytype-infrastructure-experiments/testutils/threadbuilder" //import (
"github.com/stretchr/testify/assert" // "github.com/anytypeio/go-anytype-infrastructure-experiments/testutils/threadbuilder"
"testing" // "github.com/stretchr/testify/assert"
) // "testing"
//)
func TestDocument_Build(t *testing.T) { //
thread, err := threadbuilder.NewThreadBuilderFromFile("threadbuilder/userjoinexample.yml") //func TestDocument_Build(t *testing.T) {
if err != nil { // thread, err := threadbuilder.NewThreadBuilderFromFile("threadbuilder/userjoinexample.yml")
t.Fatal(err) // if err != nil {
} // t.Fatal(err)
keychain := thread.GetKeychain() // }
accountData := &AccountData{ // keychain := thread.GetKeychain()
Identity: keychain.GetIdentity("A"), // accountData := &AccountData{
EncKey: keychain.EncryptionKeys["A"], // Identity: keychain.GetIdentity("A"),
} // EncKey: keychain.EncryptionKeys["A"],
doc := NewDocument(thread, NewPlainTextDocumentStateProvider(), accountData) // }
res, err := doc.Build() // doc := NewDocument(thread, NewPlainTextDocumentStateProvider(), accountData)
if err != nil { // res, err := doc.Build()
t.Fatal(err) // if err != nil {
} // t.Fatal(err)
// }
st := res.(*DocumentState) //
assert.Equal(t, st.Text, "some text|first") // st := res.(*DocumentState)
} // assert.Equal(t, st.Text, "some text|first")
//}
func TestDocument_Update(t *testing.T) { //
thread, err := threadbuilder.NewThreadBuilderFromFile("threadbuilder/userjoinexample.yml") //func TestDocument_Update(t *testing.T) {
if err != nil { // thread, err := threadbuilder.NewThreadBuilderFromFile("threadbuilder/userjoinexample.yml")
t.Fatal(err) // if err != nil {
} // t.Fatal(err)
keychain := thread.GetKeychain() // }
accountData := &AccountData{ // keychain := thread.GetKeychain()
Identity: keychain.GetIdentity("A"), // accountData := &AccountData{
EncKey: keychain.EncryptionKeys["A"], // Identity: keychain.GetIdentity("A"),
} // EncKey: keychain.EncryptionKeys["A"],
doc := NewDocument(thread, NewPlainTextDocumentStateProvider(), accountData) // }
res, err := doc.Build() // doc := NewDocument(thread, NewPlainTextDocumentStateProvider(), accountData)
if err != nil { // res, err := doc.Build()
t.Fatal(err) // if err != nil {
} // t.Fatal(err)
// }
st := res.(*DocumentState) //
assert.Equal(t, st.Text, "some text|first") // st := res.(*DocumentState)
// assert.Equal(t, st.Text, "some text|first")
rawChs := thread.GetUpdatedChanges() //
res, updateResult, err := doc.Update(rawChs...) // rawChs := thread.GetUpdatedChanges()
assert.Equal(t, updateResult, UpdateResultAppend) // res, updateResult, err := doc.Update(rawChs...)
assert.Equal(t, res.(*DocumentState).Text, "some text|first|second") // assert.Equal(t, updateResult, UpdateResultAppend)
} // assert.Equal(t, res.(*DocumentState).Text, "some text|first|second")
//}

View File

@ -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
}

View File

@ -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
}

View File

@ -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")
}