Documentation
¶
Overview ¶
Package io provides JSON import and export for directed acyclic graphs (DAGs).
Overview ¶
This package enables serialization of dependency graphs to and from a simple JSON format. The format is designed for:
- Visualization of any directed graph, not just package dependencies
- Integration with external tools that produce or consume graph data
- Caching of parsed dependency data for faster re-rendering
- Round-trip preservation: import, render, export, and re-import identically
JSON Format ¶
The format has two required top-level arrays:
{
"nodes": [
{"id": "app"},
{"id": "lib-a"},
{"id": "lib-b"}
],
"edges": [
{"from": "app", "to": "lib-a"},
{"from": "lib-a", "to": "lib-b"}
]
}
Node Fields ¶
Required:
- id: Unique string identifier (also used as the display label)
Optional:
- row: Pre-assigned layer (computed automatically if omitted)
- kind: Internal node type ("subdivider" or "auxiliary")
- meta: Freeform object for package metadata
Metadata Keys ¶
The meta object can contain any data, but certain keys are recognized by render features:
- repo_url: Clickable link for blocks
- repo_stars: Star count for popups
- repo_owner: Repository owner for Nebraska ranking
- repo_maintainers: Maintainer list for Nebraska ranking
- repo_last_commit: Last commit date for staleness detection
- repo_archived: Whether the repository is archived
- summary/description: Displayed in hover popups
Import ¶
Use ImportJSON to read a graph from a file path, or ReadJSON to read from any io.Reader:
g, err := io.ImportJSON("deps.json")
if err != nil {
log.Fatal(err)
}
Both functions validate the JSON structure and DAG constraints (no cycles, no duplicate node IDs). Errors are wrapped with context about which node or edge caused the problem.
Export ¶
Use ExportJSON to write a graph to a file, or WriteJSON to write to any io.Writer:
err := io.ExportJSON(g, "output.json")
if err != nil {
log.Fatal(err)
}
The export includes all node and edge data, including synthetic nodes (subdividers, auxiliaries) and their metadata. Row assignments, node kinds, and all metadata are preserved. This enables full round-trip fidelity: import a graph, transform it, export the result, and re-import identically.
Concurrency ¶
All functions in this package are safe to call concurrently with other readers of the same DAG, but not with concurrent modifications to the DAG. The ReadJSON and ImportJSON functions create independent DAG instances that can be used and modified freely after import.
Layout Export ¶
This package exports the logical graph structure only (nodes, edges, metadata). For external tools that need computed layout positions, use the JSON sink in render/tower/sink, which exports the complete [layout.Layout] including block coordinates, row orderings, and all render options.
render/tower/sink: github.com/matzehuels/stacktower/pkg/render/tower/sink [layout.Layout]: github.com/matzehuels/stacktower/pkg/render/tower/layout.Layout
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ExportJSON ¶ added in v0.2.1
ExportJSON writes a DAG to a JSON file at path.
ExportJSON creates (or truncates) the file at path and writes the JSON representation of g using WriteJSON. The file is created with 0644 permissions.
If the file cannot be created, or if writing fails, ExportJSON returns an error describing the failure. The error wraps the underlying cause with the file path for context.
This function is safe to call concurrently with other readers of g, but not with concurrent writes to g.
Example ¶
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/matzehuels/stacktower/pkg/dag"
"github.com/matzehuels/stacktower/pkg/io"
)
func main() {
// Build a simple graph
g := dag.New(nil)
_ = g.AddNode(dag.Node{ID: "server"})
_ = g.AddNode(dag.Node{ID: "database", Row: 1})
_ = g.AddEdge(dag.Edge{From: "server", To: "database"})
// Export to a file
tmpDir := os.TempDir()
path := filepath.Join(tmpDir, "exported-graph.json")
defer os.Remove(path)
if err := io.ExportJSON(g, path); err != nil {
fmt.Println("Error:", err)
return
}
// Verify the file was created
if _, err := os.Stat(path); err == nil {
fmt.Println("Graph exported successfully")
}
}
Output: Graph exported successfully
func ImportJSON ¶
ImportJSON reads a JSON file at path and returns the decoded DAG.
ImportJSON opens the file, decodes it using ReadJSON, and closes the file. If the file cannot be opened, or if decoding fails, ImportJSON returns an error describing the failure. The error wraps the underlying cause with the file path for context.
ImportJSON returns the same validation errors as ReadJSON for malformed graphs or DAG constraint violations.
Example ¶
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/matzehuels/stacktower/pkg/io"
)
func main() {
// Create a temporary JSON file
tmpDir := os.TempDir()
path := filepath.Join(tmpDir, "example-graph.json")
jsonData := []byte(`{
"nodes": [
{"id": "root"},
{"id": "child-a", "row": 1},
{"id": "child-b", "row": 1}
],
"edges": [
{"from": "root", "to": "child-a"},
{"from": "root", "to": "child-b"}
]
}`)
if err := os.WriteFile(path, jsonData, 0644); err != nil {
fmt.Println("Error:", err)
return
}
defer os.Remove(path)
// Import the graph
g, err := io.ImportJSON(path)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Imported", g.NodeCount(), "nodes")
fmt.Println("Root has", g.OutDegree("root"), "children")
}
Output: Imported 3 nodes Root has 2 children
func ReadJSON ¶
ReadJSON decodes a JSON graph from r into a DAG.
The input must be a JSON object with "nodes" and "edges" arrays:
{
"nodes": [{"id": "a"}, {"id": "b"}],
"edges": [{"from": "a", "to": "b"}]
}
Each node must have an "id" field. Optional fields:
- row: integer layer assignment (defaults to 0)
- kind: "subdivider" or "auxiliary" (defaults to normal node)
- meta: object with arbitrary key-value pairs
Each edge must have "from" and "to" fields that reference node IDs.
ReadJSON returns an error if:
- The JSON is malformed or invalid
- A node has a duplicate ID
- An edge references an unknown node ID
- Adding a node or edge violates DAG constraints (e.g., creates a cycle)
Errors are wrapped with context describing which node or edge caused the problem. Use errors.Is or errors.As to check for specific DAG errors.
The returned DAG is independent of r and can be modified safely after ReadJSON returns. ReadJSON does not close r.
Example ¶
package main
import (
"bytes"
"fmt"
"github.com/matzehuels/stacktower/pkg/io"
)
func main() {
// JSON input representing a dependency graph
jsonData := `{
"nodes": [
{"id": "app"},
{"id": "lib", "row": 1}
],
"edges": [
{"from": "app", "to": "lib"}
]
}`
// Parse the JSON
g, err := io.ReadJSON(bytes.NewReader([]byte(jsonData)))
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Nodes:", g.NodeCount())
fmt.Println("Edges:", g.EdgeCount())
fmt.Println("Children of app:", g.Children("app"))
}
Output: Nodes: 2 Edges: 1 Children of app: [lib]
Example (WithMetadata) ¶
package main
import (
"bytes"
"fmt"
"github.com/matzehuels/stacktower/pkg/io"
)
func main() {
// JSON with package metadata (as produced by dependency parsing)
jsonData := `{
"nodes": [
{
"id": "fastapi",
"meta": {
"version": "0.100.0",
"description": "FastAPI framework",
"repo_stars": 70000
}
},
{
"id": "pydantic",
"row": 1,
"meta": {
"version": "2.0.0"
}
}
],
"edges": [
{"from": "fastapi", "to": "pydantic"}
]
}`
g, _ := io.ReadJSON(bytes.NewReader([]byte(jsonData)))
node, _ := g.Node("fastapi")
fmt.Println("Package:", node.ID)
fmt.Println("Version:", node.Meta["version"])
fmt.Println("Stars:", node.Meta["repo_stars"])
}
Output: Package: fastapi Version: 0.100.0 Stars: 70000
func WriteJSON ¶
WriteJSON encodes a DAG as JSON and writes it to w.
The output is a JSON object with "nodes" and "edges" arrays, formatted with 2-space indentation. All nodes are written in their original order with:
- id: always present
- row: included only if non-zero
- kind: included only for non-default kinds (subdivider, auxiliary)
- meta: included if non-empty
Edges are written as {from, to} pairs.
The output can be read back with ReadJSON to produce an identical DAG, preserving all metadata, node kinds, and assigned row numbers.
WriteJSON returns an error if encoding fails or if writing to w fails. It does not validate the DAG structure; malformed graphs will be encoded as-is and may fail validation on import.
This function is safe to call concurrently with other readers of g, but not with concurrent writes to g.
Example ¶
package main
import (
"bytes"
"fmt"
"github.com/matzehuels/stacktower/pkg/dag"
"github.com/matzehuels/stacktower/pkg/io"
)
func main() {
// Create a simple dependency graph
g := dag.New(nil)
_ = g.AddNode(dag.Node{ID: "app", Row: 0})
_ = g.AddNode(dag.Node{ID: "lib", Row: 1, Meta: dag.Metadata{"version": "1.0.0"}})
_ = g.AddEdge(dag.Edge{From: "app", To: "lib"})
// Write to a buffer (or any io.Writer)
var buf bytes.Buffer
if err := io.WriteJSON(g, &buf); err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("JSON output:")
fmt.Println(buf.String())
}
Output: JSON output: { "nodes": [ { "id": "app" }, { "id": "lib", "row": 1, "meta": { "version": "1.0.0" } } ], "edges": [ { "from": "app", "to": "lib" } ] }
Types ¶
This section is empty.