From e734fc77ed1d6816e3f569123df60ecfb9d490ef Mon Sep 17 00:00:00 2001 From: Jay Date: Tue, 3 Mar 2026 11:01:37 -0500 Subject: [PATCH] Wrote simple event to subgraph function. Updated related code to align. --- event.go | 61 +++++++++++++++ event_test.go | 204 +++++++++++++++++++++++++++++++++++++++++++++++++ graph.go | 8 ++ graph_test.go | 2 +- schema.go | 39 ++++------ schema_test.go | 31 ++++---- util.go | 16 ++++ 7 files changed, 319 insertions(+), 42 deletions(-) create mode 100644 event.go create mode 100644 event_test.go diff --git a/event.go b/event.go new file mode 100644 index 0000000..aa5a803 --- /dev/null +++ b/event.go @@ -0,0 +1,61 @@ +package heartwood + +import ( + roots "git.wisehodl.dev/jay/go-roots/events" +) + +func EventToSubgraph(e roots.Event) *Subgraph { + subgraph := NewSubgraph() + + // Create Event node + eventNode := NewEventNode(e.ID) + eventNode.Props["created_at"] = e.CreatedAt + eventNode.Props["kind"] = e.Kind + eventNode.Props["content"] = e.Content + + // Create User node + userNode := NewUserNode(e.PubKey) + + // Create SIGNED rel + signedRel := NewSignedRel(userNode, eventNode, nil) + + // Create Tag nodes + tagNodes := []*Node{} + for _, tag := range e.Tags { + if !isValidTag(tag) { + continue + } + tagNodes = append(tagNodes, NewTagNode(tag[0], tag[1])) + } + + // Create Tag rels + tagRels := []*Relationship{} + for _, tagNode := range tagNodes { + tagRels = append(tagRels, NewTaggedRel(eventNode, tagNode, nil)) + } + + // Populate subgraph + subgraph.AddNode(eventNode) + subgraph.AddNode(userNode) + subgraph.AddRel(signedRel) + for _, node := range tagNodes { + subgraph.AddNode(node) + } + for _, rel := range tagRels { + subgraph.AddRel(rel) + } + + return subgraph +} + +func isValidTag(t roots.Tag) bool { + if len(t) < 2 { + // Skip tags that do not have name and value fields + return false + } + if len(t[0])+len(t[1]) > 8192 { + // Skip tags that are too large for the neo4j indexer + return false + } + return true +} diff --git a/event_test.go b/event_test.go new file mode 100644 index 0000000..6a6e1a9 --- /dev/null +++ b/event_test.go @@ -0,0 +1,204 @@ +package heartwood + +import ( + "fmt" + roots "git.wisehodl.dev/jay/go-roots/events" + "github.com/stretchr/testify/assert" + "reflect" + "testing" +) + +var ids = map[string]string{ + "a": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "b": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "c": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "d": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", +} + +var static = roots.Event{ + CreatedAt: 1000, + Kind: 1, + Content: "hello", +} + +func newFullEventNode(id string, createdAt, kind int, content string) *Node { + n := NewEventNode(id) + n.Props["created_at"] = createdAt + n.Props["kind"] = kind + n.Props["content"] = content + return n +} + +func baseSubgraph(eventID, pubkey string) (*Subgraph, *Node, *Node) { + s := NewSubgraph() + eventNode := newFullEventNode(eventID, static.CreatedAt, static.Kind, static.Content) + userNode := NewUserNode(pubkey) + s.AddNode(eventNode) + s.AddNode(userNode) + s.AddRel(NewSignedRel(userNode, eventNode, nil)) + return s, eventNode, userNode +} + +func TestEventToSubgraph(t *testing.T) { + cases := []struct { + name string + event roots.Event + expected *Subgraph + }{ + { + name: "bare event", + event: roots.Event{ + ID: ids["a"], PubKey: ids["b"], + CreatedAt: static.CreatedAt, Kind: static.Kind, Content: static.Content, + }, + expected: func() *Subgraph { + s, _, _ := baseSubgraph(ids["a"], ids["b"]) + return s + }(), + }, + { + name: "single generic tag", + event: roots.Event{ + ID: ids["a"], PubKey: ids["b"], + CreatedAt: static.CreatedAt, Kind: static.Kind, Content: static.Content, + Tags: []roots.Tag{{"t", "bitcoin"}}, + }, + expected: func() *Subgraph { + s, eventNode, _ := baseSubgraph(ids["a"], ids["b"]) + tagNode := NewTagNode("t", "bitcoin") + s.AddNode(tagNode) + s.AddRel(NewTaggedRel(eventNode, tagNode, nil)) + return s + }(), + }, + { + name: "tag with fewer than 2 elements", + event: roots.Event{ + ID: ids["a"], PubKey: ids["b"], + CreatedAt: static.CreatedAt, Kind: static.Kind, Content: static.Content, + Tags: []roots.Tag{{"t"}}, + }, + expected: func() *Subgraph { + s, _, _ := baseSubgraph(ids["a"], ids["b"]) + return s + }(), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := EventToSubgraph(tc.event) + assertSubgraphsEqual(t, tc.expected, got) + }) + } +} + +// helpers + +func nodesEqual(expected, got *Node) error { + // Compare label counts + if expected.Labels.Length() != got.Labels.Length() { + return fmt.Errorf( + "number of labels does not match. expected %d, got %d", + expected.Labels.Length(), got.Labels.Length()) + } + + // Compare label values + for _, label := range expected.Labels.ToArray() { + if !got.Labels.Contains(label) { + return fmt.Errorf("missing label %q", label) + } + } + + // Compare property values + if err := propsEqual(expected.Props, got.Props); err != nil { + return err + } + + return nil +} + +func relsEqual(expected, got *Relationship) error { + // Compare type + if expected.Type != got.Type { + return fmt.Errorf("type: expected %q, got %q", expected.Type, got.Type) + } + + // Compare property values + if err := propsEqual(expected.Props, got.Props); err != nil { + return err + } + + // Compare endpoints + if err := nodesEqual(expected.Start, got.Start); err != nil { + return fmt.Errorf("start node: %w", err) + } + if err := nodesEqual(expected.End, got.End); err != nil { + return fmt.Errorf("end node: %w", err) + } + + return nil +} + +func propsEqual(expected, got Properties) error { + if len(expected) != len(got) { + return fmt.Errorf( + "number of props does not match. expected %d, got %d", + len(expected), len(got)) + } + + for key, expectedVal := range expected { + gotVal, exists := got[key] + if !exists { + return fmt.Errorf("missing prop %q", key) + } + if !reflect.DeepEqual(expectedVal, gotVal) { + return fmt.Errorf("prop %q: expected %v, got %v", key, expectedVal, gotVal) + } + } + return nil +} + +func assertSubgraphsEqual(t *testing.T, expected, got *Subgraph) { + t.Helper() + + gotNodes := make([]*Node, len(got.Nodes())) + copy(gotNodes, got.Nodes()) + + gotRels := make([]*Relationship, len(got.Rels())) + copy(gotRels, got.Rels()) + + for _, expectedNode := range expected.Nodes() { + index := findInList(expectedNode, gotNodes, nodesEqual) + if index == -1 { + assert.Fail(t, fmt.Sprintf("missing expected node: %+v", expectedNode)) + continue + } + gotNodes = removeFromList(index, gotNodes) + } + + for _, expectedRel := range expected.Rels() { + index := findInList(expectedRel, gotRels, relsEqual) + if index == -1 { + assert.Fail(t, fmt.Sprintf("missing expected rel: %+v", expectedRel)) + continue + } + gotRels = removeFromList(index, gotRels) + } + + assert.Empty(t, gotNodes, "unexpected nodes in subgraph") + assert.Empty(t, gotRels, "unexpected rels in subgraph") +} + +func findInList[T any](item *T, list []*T, equal func(*T, *T) error) int { + for i, candidate := range list { + if equal(item, candidate) == nil { + return i + } + } + return -1 +} + +func removeFromList[T any](i int, list []*T) []*T { + return append(list[:i], list[i+1:]...) +} diff --git a/graph.go b/graph.go index ef32432..ebc5bec 100644 --- a/graph.go +++ b/graph.go @@ -191,6 +191,14 @@ func (s *Subgraph) AddRel(rel *Relationship) { s.rels = append(s.rels, rel) } +func (s *Subgraph) Nodes() []*Node { + return s.nodes +} + +func (s *Subgraph) Rels() []*Relationship { + return s.rels +} + // ======================================== // Structured Subgraph // ======================================== diff --git a/graph_test.go b/graph_test.go index b2f33ec..23af79b 100644 --- a/graph_test.go +++ b/graph_test.go @@ -161,7 +161,7 @@ func TestStructuredSubgraphAddRel(t *testing.T) { userNode := NewUserNode("pubkey1") eventNode := NewEventNode("abc123") - rel, _ := NewSignedRel(userNode, eventNode, nil) + rel := NewSignedRel(userNode, eventNode, nil) err := subgraph.AddRel(rel) diff --git a/schema.go b/schema.go index 628319e..2787334 100644 --- a/schema.go +++ b/schema.go @@ -40,11 +40,10 @@ func NewEventNode(id string) *Node { return NewNode("Event", Properties{"id": id}) } -func NewTagNode(name string, value string, rest []string) *Node { +func NewTagNode(name string, value string) *Node { return NewNode("Tag", Properties{ "name": name, - "value": value, - "rest": rest}) + "value": value}) } // ======================================== @@ -52,26 +51,26 @@ func NewTagNode(name string, value string, rest []string) *Node { // ======================================== func NewSignedRel( - start *Node, end *Node, props Properties) (*Relationship, error) { + start *Node, end *Node, props Properties) *Relationship { return NewRelationshipWithValidation( "SIGNED", "User", "Event", start, end, props) } func NewTaggedRel( - start *Node, end *Node, props Properties) (*Relationship, error) { + start *Node, end *Node, props Properties) *Relationship { return NewRelationshipWithValidation( "TAGGED", "Event", "Tag", start, end, props) } func NewReferencesEventRel( - start *Node, end *Node, props Properties) (*Relationship, error) { + start *Node, end *Node, props Properties) *Relationship { return NewRelationshipWithValidation( "REFERENCES", "Tag", "Event", start, end, props) } func NewReferencesUserRel( - start *Node, end *Node, props Properties) (*Relationship, error) { + start *Node, end *Node, props Properties) *Relationship { return NewRelationshipWithValidation( "REFERENCES", "Tag", "User", start, end, props) } @@ -80,15 +79,13 @@ func NewReferencesUserRel( // Relationship Constructor Helpers // ======================================== -func validateNodeLabel(node *Node, role string, expectedLabel string) error { +func validateNodeLabel(node *Node, role string, expectedLabel string) { if !node.Labels.Contains(expectedLabel) { - return fmt.Errorf( - "expected %s node to have label '%s'. got %v", + panic(fmt.Errorf( + "expected %s node to have label %q. got %v", role, expectedLabel, node.Labels.ToArray(), - ) + )) } - - return nil } func NewRelationshipWithValidation( @@ -97,20 +94,12 @@ func NewRelationshipWithValidation( endLabel string, start *Node, end *Node, - props Properties) (*Relationship, error) { - var err error + props Properties) *Relationship { - err = validateNodeLabel(start, "start", startLabel) - if err != nil { - return nil, err - } + validateNodeLabel(start, "start", startLabel) + validateNodeLabel(end, "end", endLabel) - err = validateNodeLabel(end, "end", endLabel) - if err != nil { - return nil, err - } - - return NewRelationship(rtype, start, end, props), nil + return NewRelationship(rtype, start, end, props) } // ======================================== diff --git a/schema_test.go b/schema_test.go index 025fbce..0f19065 100644 --- a/schema_test.go +++ b/schema_test.go @@ -7,11 +7,11 @@ import ( func TestNewRelationshipWithValidation(t *testing.T) { cases := []struct { - name string - start *Node - end *Node - wantErr bool - wantErrText string + name string + start *Node + end *Node + wantPanic bool + wantPanicText string }{ { name: "valid start and end nodes", @@ -19,24 +19,23 @@ func TestNewRelationshipWithValidation(t *testing.T) { end: NewEventNode("abc123"), }, { - name: "mismatched start node label", - start: NewEventNode("abc123"), - end: NewEventNode("abc123"), - wantErr: true, - wantErrText: "expected start node to have label 'User'", + name: "mismatched start node label", + start: NewEventNode("abc123"), + end: NewEventNode("abc123"), + wantPanic: true, + wantPanicText: "expected start node to have label \"User\". got [Event]", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - rel, err := NewSignedRel(tc.start, tc.end, nil) - if tc.wantErr { - assert.Error(t, err) - assert.ErrorContains(t, err, tc.wantErrText) - assert.Nil(t, rel) + if tc.wantPanic { + assert.PanicsWithError(t, tc.wantPanicText, func() { + NewSignedRel(tc.start, tc.end, nil) + }) return } - assert.NoError(t, err) + rel := NewSignedRel(tc.start, tc.end, nil) assert.Equal(t, "SIGNED", rel.Type) assert.Contains(t, rel.Start.Labels.ToArray(), "User") assert.Contains(t, rel.End.Labels.ToArray(), "Event") diff --git a/util.go b/util.go index 6a944f0..b68fc6c 100644 --- a/util.go +++ b/util.go @@ -29,6 +29,22 @@ func (s Set[T]) Contains(item T) bool { return exists } +func (s Set[T]) Equal(other Set[T]) bool { + if len(s.inner) != len(other.inner) { + return false + } + for item := range s.inner { + if !other.Contains(item) { + return false + } + } + return true +} + +func (s Set[T]) Length() int { + return len(s.inner) +} + func (s Set[T]) ToArray() []T { array := []T{} for i := range s.inner {