Quickstart
This chapter walks through building, manipulating, and querying Qubes using the Rust API. For the equivalent Python API, see the Python bindings chapter.
Creating a Qube
From ASCII Representation
The most readable way to build a Qube is from its ASCII tree representation:
#![allow(unused)]
fn main() {
use qubed::Qube;
let q = Qube::from_ascii(r#"root
├── class=od
│ ├── expver=0001
│ │ ├── param=1
│ │ └── param=2
│ └── expver=0002
│ ├── param=1
│ └── param=2
└── class=rd
├── expver=0001
│ ├── param=1
│ ├── param=2
│ └── param=3
└── expver=0002
├── param=1
└── param=2"#).unwrap();
println!("{}", q.to_ascii());
}
Each line takes the form key=value where multiple values are separated by /:
root
├── class=od, expver=0001/0002, param=1/2
From Nested JSON
You can also build a Qube from a JSON object where keys are "dimension=values" strings:
#![allow(unused)]
fn main() {
use qubed::Qube;
use serde_json::json;
let q = Qube::from_json(json!({
"class=od": {
"expver=0001/0002": {
"param=1/2": {}
}
},
"class=rd": {
"expver=0001": { "param=1/2/3": {} },
"expver=0002": { "param=1/2": {} }
}
})).unwrap();
}
Programmatically
Build a Qube node by node:
#![allow(unused)]
fn main() {
use qubed::{Qube, Coordinates};
let mut q = Qube::new();
let root = q.root();
// Create coordinate values
let class_coords = Coordinates::from_string("od");
let child = q.create_child("class", root, Some(class_coords)).unwrap();
let exp_coords = Coordinates::from_string("0001/0002");
let exp = q.create_child("expver", child, Some(exp_coords)).unwrap();
let param_coords = Coordinates::from_string("1/2");
q.create_child("param", exp, Some(param_coords)).unwrap();
}
From a Datacube
Build a Qube from a Datacube (a flat map of dimensions to coordinates):
#![allow(unused)]
fn main() {
use qubed::{Datacube, Qube, Coordinates};
let mut dc = Datacube::new();
dc.add_coordinate("class", Coordinates::from_string("od/rd"));
dc.add_coordinate("expver", Coordinates::from_string("0001/0002"));
dc.add_coordinate("param", Coordinates::from_string("1/2"));
let order = vec!["class".to_string(), "expver".to_string(), "param".to_string()];
let q = Qube::from_datacube(&dc, Some(&order));
}
Compression
Compression merges sibling nodes with identical subtree structure. This is the defining operation of Qubed — it keeps trees compact without losing information.
#![allow(unused)]
fn main() {
let mut q = Qube::from_ascii(r#"root
├── class=od
│ ├── expver=0001
│ │ ├── param=1
│ │ └── param=2
│ └── expver=0002
│ ├── param=1
│ └── param=2
└── class=rd
├── expver=0001
│ ├── param=1
│ └── param=2
└── expver=0002
├── param=1
└── param=2"#).unwrap();
q.compress();
println!("{}", q.to_ascii());
// root
// └── class=od/rd, expver=0001/0002, param=1/2
}
After compression, the number of leaf identifiers is preserved but the tree has far fewer nodes.
Selection
Select a subset of the tree by providing dimension constraints:
#![allow(unused)]
fn main() {
use qubed::Qube;
use qubed::select::SelectMode;
let q = Qube::from_ascii(r#"root
├── class=od
│ ├── expver=0001
│ │ ├── param=1
│ │ └── param=2
│ └── expver=0002
│ ├── param=1
│ └── param=2
└── class=rd
├── expver=0001
│ ├── param=1
│ ├── param=2
│ └── param=3
└── expver=0002
├── param=1
└── param=2"#).unwrap();
// Select only class=od, param=1
let selection = [("class", &[1]), ("param", &[1])];
let result = q.select(&selection, SelectMode::Default).unwrap();
println!("{}", result.to_ascii());
}
SelectMode::Prune additionally removes branches that don’t contain all selected dimensions.
SelectMode::Strict returns the same result as Default, but returns an error if any requested value can’t be found. Each value only needs to exist in one branch that the selection reaches, not in every branch:
#![allow(unused)]
fn main() {
// Err: param=4 does not exist anywhere in the qube.
assert!(q.select(&[("class", &[1][..]), ("param", &[1, 4][..])], SelectMode::Strict).is_err());
}
In Python, pass the mode as a string: q.select({"param": [1, 4]}, "strict") raises ValueError.
Union (Append)
Merge two Qubes together. The result contains all identifiers from both:
#![allow(unused)]
fn main() {
let mut a = Qube::from_ascii(r#"root
└── class=od, expver=0001, param=1/2"#).unwrap();
let mut b = Qube::from_ascii(r#"root
└── class=rd, expver=0002, param=3/4"#).unwrap();
a.append(&mut b);
// b is now empty; a contains the union, automatically compressed
println!("{}", a.to_ascii());
}
For merging many Qubes at once, append_many is more efficient — it performs intermediate compression every 500 merges:
#![allow(unused)]
fn main() {
let mut base = Qube::new();
let mut others: Vec<Qube> = vec![/* ... */];
base.append_many(&mut others);
}
Iteration
Datacubes
Decompose the Qube back into individual dense datacubes. Each datacube is a HashMap<String, Coordinates>:
#![allow(unused)]
fn main() {
let datacubes = q.to_datacubes();
for dc in &datacubes {
for (dim, coords) in dc.coordinates() {
println!(" {} = {}", dim, coords.to_string());
}
}
}
Leaf Count
Get the number of individual identifiers without expanding:
#![allow(unused)]
fn main() {
let count = q.datacube_count();
println!("This qube contains {} identifiers", count);
}
Serialization
ASCII
Human-readable tree format, useful for debugging and display:
#![allow(unused)]
fn main() {
let ascii = q.to_ascii();
let roundtrip = Qube::from_ascii(&ascii).unwrap();
}
Nested JSON
Keys are "dimension=values" strings, values are child objects:
#![allow(unused)]
fn main() {
let json_val = q.to_json();
let json_str = serde_json::to_string_pretty(&json_val).unwrap();
let roundtrip = Qube::from_json(json_val).unwrap();
}
Arena JSON
A flat BFS array of node records — more suitable for programmatic consumption and web transport:
#![allow(unused)]
fn main() {
let arena = q.to_arena_json();
// Each entry: { "dim": "class", "coords": "od/rd", "parent": 0, "children": [1, 2] }
let restored = Qube::from_arena_json(arena).unwrap();
}
Each node in the array contains:
dim— dimension name (e.g."class")coords— coordinate values as a/-separated stringparent— index of the parent node (ornullfor root)children— array of child node indices
Coordinate Types
The Coordinates enum supports multiple value types and automatically categorizes them:
| Variant | Stores | Example |
|---|---|---|
Empty | No values | (default for root) |
Integers | Sorted i32 values | 1/2/3 |
Floats | Sorted f64 values | 0.1/0.5 |
Strings | Sorted string values | od/rd |
Mixed | Combination of above | 1/od/0.5 |
Leading Zero Preservation
Values with leading zeros (like "0001") are preserved as strings rather than parsed as integers. The detection logic: if a token has length > 1, starts with '0', and the second character is a digit, it’s stored as a String.
#![allow(unused)]
fn main() {
let coords = Coordinates::from_string("0001/0002");
assert_eq!(coords.to_string(), "0001/0002"); // NOT "1/2"
}
Operations on Coordinates
#![allow(unused)]
fn main() {
use qubed::Coordinates;
// Append values
let mut c = Coordinates::new();
c.append(1_i32);
c.append(2_i32);
// Extend from another
let other = Coordinates::from_string("3/4");
c.extend(&other);
// Intersect
let a = Coordinates::from_string("1/2/3");
let b = Coordinates::from_string("2/3/4");
let result = a.intersect(&b);
// result.intersection = [2, 3]
// result.only_a = [1]
// result.only_b = [4]
}
Tree Inspection
#![allow(unused)]
fn main() {
// Check if a qube has any content
let is_empty = q.is_empty();
// Get the number of leaf datacubes
let n = q.datacube_count();
// Get all unique dimension→coordinates pairs across the entire tree
let all = q.all_unique_dim_coords();
for (dim, coords) in &all {
println!("{}: {}", dim, coords.to_string());
}
}
Node Navigation
Access individual nodes via NodeRef:
#![allow(unused)]
fn main() {
let root = q.root();
let root_node = q.node(root).unwrap();
// Dimension name
let dim = root_node.dimension(); // Some("root")
// Coordinates
let coords = root_node.coordinates();
// Iterate children
for child_id in root_node.all_children() {
let child = q.node(child_id).unwrap();
println!("{} = {}", child.dimension().unwrap(), child.coordinates().to_string());
}
// Get all dimensions in subtree
let dims = root_node.span();
// Walk ancestors
for ancestor_id in root_node.ancestors() {
// ...
}
}