From 45590c0e23bad4a139b021413398d4e4f3d07d8e Mon Sep 17 00:00:00 2001 From: Adan Santos Date: Fri, 27 Mar 2026 04:07:43 -0500 Subject: [PATCH] intent graph implementation and parse test with matts json example --- napkin-app/src/test_data/graph.json | 60 ++++++++++++ .../graph/{graph.go => directed_graph.go} | 0 napkin-backend/graph/graph_test.go | 72 ++++++++++++++ napkin-backend/graph/intent_graph.go | 96 +++++++++++++++++++ napkin-backend/graph/type.go | 3 + napkin-backend/output.tf | 2 +- napkin-backend/test_data/graph.json | 60 ++++++++++++ 7 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 napkin-app/src/test_data/graph.json rename napkin-backend/graph/{graph.go => directed_graph.go} (100%) create mode 100644 napkin-backend/graph/intent_graph.go create mode 100644 napkin-backend/test_data/graph.json diff --git a/napkin-app/src/test_data/graph.json b/napkin-app/src/test_data/graph.json new file mode 100644 index 0000000..6beef4b --- /dev/null +++ b/napkin-app/src/test_data/graph.json @@ -0,0 +1,60 @@ +{ + "title": "Spring '26 Schema", + "type": "object", + "properties": { + "nodes": { + "Server": { + "type": "ec2", + "properties": { + "id": "string", + "name": "string", + "instanceType": "string", + "region": "string" + }, + "ports": { + "inputs": { + "in-env": "data", + "in-network": "network" + }, + "outputs": { + "out-network": "network", + "out-data": "data" + } + } + }, + "Database": { + "properties": { + "id": "string", + "name": "string", + "engine": { + "type": "string", + "enum": ["mysql", "postgres", "mongodb", "oracle"] + }, + "storageSize": "number", + "region": "string" + }, + "ports": { + "inputs": { + "in-network": "network" + }, + "outputs": { + "out-conn": "data" + } + } + } + }, + "edges": { + "ServerToDatabase": { + "source": { + "node": "Server", + "port": "out-data" + }, + "target": { + "node": "Database", + "port": "in-network" + }, + "type": "data-flow" + } + } + } +} \ No newline at end of file diff --git a/napkin-backend/graph/graph.go b/napkin-backend/graph/directed_graph.go similarity index 100% rename from napkin-backend/graph/graph.go rename to napkin-backend/graph/directed_graph.go diff --git a/napkin-backend/graph/graph_test.go b/napkin-backend/graph/graph_test.go index de24585..b7d7400 100644 --- a/napkin-backend/graph/graph_test.go +++ b/napkin-backend/graph/graph_test.go @@ -1,6 +1,7 @@ package graph import ( + "encoding/json" "fmt" "testing" ) @@ -74,4 +75,75 @@ func TestFromJSON(t *testing.T) { t.Fatal("expected api -> db edge") } fmt.Println(g) +} + +func TestIntentGraphWithSpring26Schema(t *testing.T) { + rawJSON := `{ + "nodes": [ + { + "id": "srv-01", + "type": "Server", + "data": { + "instanceType": "t3.medium", + "region": "us-east-1", + "name": "Production-API", + "ports": { + "inputs": ["in-env", "in-network"], + "outputs": ["out-network", "out-data"] + } + } + }, + { + "id": "db-01", + "type": "Database", + "data": { + "engine": "postgres", + "storageSize": 100, + "region": "us-east-1", + "ports": { + "inputs": ["in-network"], + "outputs": ["out-conn"] + } + } + } + ], + "edges": [ + { + "source": "srv-01", + "target": "db-01" + } + ] + }` + + ig := NewIntentGraph() + + err := ig.FromJSON([]byte(rawJSON)) + if err != nil { + t.Fatalf("Failed to parse Spring '26 instance: %v", err) + } + + prettyJSON, _ := json.MarshalIndent(ig, "", " ") + fmt.Printf("IntentGraph Structure:\n%s\n", string(prettyJSON)) + + srvNode, exists := ig.Nodes["srv-01"] + if !exists { + t.Fatal("Server node srv-01 was not found in IntentGraph") + } + + if srvNode.Data["instanceType"] != "t3.medium" { + t.Errorf("Expected instanceType t3.medium, got %v", srvNode.Data["instanceType"]) + } + + if !ig.HasEdge("srv-01", "db-01") { + t.Error("Edge between Server and Database not found") + } + + dg, err := ig.ToDirectedGraph() + if err != nil { + t.Fatalf("Failed to translate to DirectedGraph: %v", err) + } + + if !dg.HasNode("db-01") { + t.Error("DirectedGraph failed to import the Database node") + } } \ No newline at end of file diff --git a/napkin-backend/graph/intent_graph.go b/napkin-backend/graph/intent_graph.go new file mode 100644 index 0000000..23038c1 --- /dev/null +++ b/napkin-backend/graph/intent_graph.go @@ -0,0 +1,96 @@ +package graph + +import ( + "encoding/json" + "errors" +) + +type IntentGraph struct { + Nodes map[NodeID]Node + Edges []Edge +} + +func NewIntentGraph() *IntentGraph { + return &IntentGraph{ + Nodes: make(map[NodeID]Node), + Edges: []Edge{}, + } +} + +func (ig *IntentGraph) FromJSON(data []byte) error { + var payload GraphJSON + if err := json.Unmarshal(data, &payload); err != nil { + return err + } + + for _, n := range payload.Nodes { + ig.Nodes[n.ID] = n + } + ig.Edges = payload.Edges + return nil +} + +func (ig *IntentGraph) ToDirectedGraph() (*DirectedGraph, error) { + dg := NewDirectedGraph() + + for _, node := range ig.Nodes { + if err := dg.AddNode(node); err != nil { + return nil, err + } + } + + for _, edge := range ig.Edges { + if err := dg.AddEdge(edge); err != nil { + return nil, err + } + } + + return dg, nil +} + +func (ig *IntentGraph) AddNode(node Node) error { + ig.Nodes[node.ID] = node + return nil +} + +func (ig *IntentGraph) AddEdge(edge Edge) error { + ig.Edges = append(ig.Edges, edge) + return nil +} + +func (ig *IntentGraph) HasNode(id NodeID) bool { + _, ok := ig.Nodes[id] + return ok +} + +func (ig *IntentGraph) HasEdge(from, to NodeID) bool { + for _, e := range ig.Edges { + if e.From == from && e.To == to { + return true + } + } + return false +} + +func (ig *IntentGraph) RemoveNode(id NodeID) error { + delete(ig.Nodes, id) + // Remove associated edges + newEdges := []Edge{} + for _, e := range ig.Edges { + if e.From != id && e.To != id { + newEdges = append(newEdges, e) + } + } + ig.Edges = newEdges + return nil +} + +func (ig *IntentGraph) RemoveEdge(from, to NodeID) error { + for i, e := range ig.Edges { + if e.From == from && e.To == to { + ig.Edges = append(ig.Edges[:i], ig.Edges[i+1:]...) + return nil + } + } + return errors.New("edge not found") +} \ No newline at end of file diff --git a/napkin-backend/graph/type.go b/napkin-backend/graph/type.go index e993639..4bff989 100644 --- a/napkin-backend/graph/type.go +++ b/napkin-backend/graph/type.go @@ -11,6 +11,9 @@ type Node struct { type Edge struct { From NodeID `json:"source"` To NodeID `json:"target"` + SourcePort string `json:"sourcePort,omitempty"` + TargetPort string `json:"targetPort,omitempty"` + Type string `json:"type,omitempty"` } type GraphJSON struct { diff --git a/napkin-backend/output.tf b/napkin-backend/output.tf index 5d7bf79..2cf316d 100644 --- a/napkin-backend/output.tf +++ b/napkin-backend/output.tf @@ -1,8 +1,8 @@ terraform { required_providers { aws = { - version = "~> 5.0" source = "hashicorp/aws" + version = "~> 5.0" } } } diff --git a/napkin-backend/test_data/graph.json b/napkin-backend/test_data/graph.json new file mode 100644 index 0000000..6beef4b --- /dev/null +++ b/napkin-backend/test_data/graph.json @@ -0,0 +1,60 @@ +{ + "title": "Spring '26 Schema", + "type": "object", + "properties": { + "nodes": { + "Server": { + "type": "ec2", + "properties": { + "id": "string", + "name": "string", + "instanceType": "string", + "region": "string" + }, + "ports": { + "inputs": { + "in-env": "data", + "in-network": "network" + }, + "outputs": { + "out-network": "network", + "out-data": "data" + } + } + }, + "Database": { + "properties": { + "id": "string", + "name": "string", + "engine": { + "type": "string", + "enum": ["mysql", "postgres", "mongodb", "oracle"] + }, + "storageSize": "number", + "region": "string" + }, + "ports": { + "inputs": { + "in-network": "network" + }, + "outputs": { + "out-conn": "data" + } + } + } + }, + "edges": { + "ServerToDatabase": { + "source": { + "node": "Server", + "port": "out-data" + }, + "target": { + "node": "Database", + "port": "in-network" + }, + "type": "data-flow" + } + } + } +} \ No newline at end of file