Skip to content

Commit 90ea736

Browse files
committed
flesh out rust implementation
1 parent 959dac3 commit 90ea736

File tree

10 files changed

+484
-224
lines changed

10 files changed

+484
-224
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ serde = { version = "1.0", features = ["derive"] }
1010
serde_json = "1.0"
1111
pyo3 = "0.25"
1212
lasso = "0.7.3"
13+
itertools = "0.14.0"
1314

1415
[package.metadata.maturin]
1516
version-from-git = true

src/rust/formatters/mod.rs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
use crate::{Node, NodeId, Qube};
2+
use itertools::Itertools;
3+
use itertools::Position;
4+
5+
impl Node {
6+
/// Generate a human readable summary of the node
7+
/// Examples include: key=value1/value2/.../valueN, key=value1/to/value1, key=*, root etc
8+
pub fn summary(&self, qube: &Qube) -> String {
9+
if self.is_root() {
10+
return "root".to_string();
11+
}
12+
let key = &qube[self.key];
13+
let values: String =
14+
Itertools::intersperse(self.values.iter().map(|id| &qube[*id]), "/").collect();
15+
16+
format!("{}={}", key, values)
17+
}
18+
19+
pub fn html_summary(&self, qube: &Qube) -> String {
20+
if self.is_root() {
21+
return r#"<span class="qubed-node">root</span>"#.to_string();
22+
}
23+
let key = &qube[self.key];
24+
let values: String =
25+
Itertools::intersperse(self.values.iter().map(|id| &qube[*id]), "/").collect();
26+
27+
let summary = format!("{}={}", key, values);
28+
let path = summary.clone();
29+
let info = format!("is_root: {}", self.is_root());
30+
format!(r#"<span class="qubed-node" data-path="{path}" title="{info}">{summary}</span>"#)
31+
}
32+
}
33+
34+
struct NodeSummary {
35+
summary: String,
36+
end: NodeId,
37+
}
38+
39+
enum SummaryType {
40+
PlainText,
41+
HTML,
42+
}
43+
44+
/// Given a Node, traverse the tree until a node has more than one child.
45+
/// Returns a summary of the form "key1=v1/v2, key2=v1/v2/v3, key3=v1"
46+
/// and the id of the last node in the summary
47+
fn summarise_nodes(qube: &Qube, node_id: &NodeId, summary_type: SummaryType) -> NodeSummary {
48+
let mut node_id = *node_id;
49+
let mut summary_vec = vec![];
50+
loop {
51+
let node = &qube[node_id];
52+
let summary = match summary_type {
53+
SummaryType::PlainText => node.summary(&qube),
54+
SummaryType::HTML => node.html_summary(&qube),
55+
};
56+
summary_vec.push(summary);
57+
58+
// Bail out if the node has anothing other than 1 child.
59+
match node.has_exactly_one_child() {
60+
Some(n) => node_id = n,
61+
None => break,
62+
};
63+
}
64+
NodeSummary {
65+
summary: summary_vec.join(", "),
66+
end: node_id,
67+
}
68+
}
69+
70+
fn qube_to_tree(qube: &Qube, node_id: &NodeId, prefix: &str, depth: usize) -> String {
71+
let NodeSummary {
72+
summary,
73+
end: node_id,
74+
} = summarise_nodes(qube, node_id, SummaryType::PlainText);
75+
76+
let mut output: Vec<String> = Vec::new();
77+
78+
if depth <= 0 {
79+
return format!("{} - ...\n", summary);
80+
} else {
81+
output.push(format!("{}\n", summary));
82+
}
83+
84+
let node = &qube[node_id];
85+
for (position, child_id) in node.children().with_position() {
86+
let (connector, extension) = match position {
87+
Position::Last | Position::Only => ("└── ", " "),
88+
_ => ("├── ", "│ "),
89+
};
90+
output.extend([
91+
prefix.to_string(),
92+
connector.to_string(),
93+
qube_to_tree(qube, child_id, &format!("{prefix}{extension}"), depth - 1),
94+
]);
95+
}
96+
97+
output.join("")
98+
}
99+
100+
fn qube_to_html(qube: &Qube, node_id: &NodeId, prefix: &str, depth: usize) -> String {
101+
let NodeSummary {
102+
summary,
103+
end: node_id,
104+
} = summarise_nodes(qube, node_id, SummaryType::PlainText);
105+
106+
let node = &qube[node_id];
107+
let mut output: Vec<String> = Vec::new();
108+
109+
let open = if depth > 0 { "open" } else { "" };
110+
output.push(format!(
111+
r#"<details {open}><summary class="qubed-level">{summary}</summary>"#
112+
));
113+
114+
for (position, child_id) in node.children().with_position() {
115+
let (connector, extension) = match position {
116+
Position::Last | Position::Only => ("└── ", " "),
117+
_ => ("├── ", "│ "),
118+
};
119+
output.extend([
120+
prefix.to_string(),
121+
connector.to_string(),
122+
qube_to_tree(qube, child_id, &format!("{prefix}{extension}"), depth - 1),
123+
]);
124+
}
125+
126+
output.join("")
127+
}
128+
129+
impl Qube {
130+
/// Return a string version of the Qube in the format
131+
/// root
132+
/// ├── class=od, expver=0001/0002, param=1/2
133+
/// └── class=rd, param=1/2/3
134+
pub fn string_tree(&self) -> String {
135+
qube_to_tree(&self, &self.root, "", 5)
136+
}
137+
138+
/// Return an HTML version of the Qube which renders like this
139+
/// root
140+
/// ├── class=od, expver=0001/0002, param=1/2
141+
/// └── class=rd, param=1/2/3
142+
/// But under the hood children are represented with a details/summary tag and each key=value is a span
143+
/// CSS and JS functionality is bundled inside.
144+
pub fn html_tree(&self) -> String {
145+
qube_to_html(&self, &self.root, "", 5)
146+
}
147+
}

src/rust/lib.rs

Lines changed: 170 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,180 @@
33
use pyo3::prelude::*;
44
use pyo3::wrap_pyfunction;
55
use pyo3::types::{PyDict, PyInt, PyList, PyString};
6+
use std::collections::HashMap;
7+
use pyo3::prelude::*;
8+
use std::hash::Hash;
9+
10+
11+
use lasso::{Rodeo, Spur};
12+
use std::num::NonZero;
13+
use std::ops;
14+
15+
mod serialisation;
16+
mod python_interface;
17+
mod formatters;
18+
19+
// This data structure uses the Newtype Index Pattern
20+
// See https://matklad.github.io/2018/06/04/newtype-index-pattern.html
21+
// See also https://github.com/nrc/r4cppp/blob/master/graphs/README.md#rcrefcellnode for a discussion of other approaches to trees and graphs in rust.
22+
// https://smallcultfollowing.com/babysteps/blog/2015/04/06/modeling-graphs-in-rust-using-vector-indices/
23+
24+
// Index types use struct Id(NonZero<usize>)
25+
// This reserves 0 as a special value which allows Option<Id(NonZero<usize>)> to be the same size as usize.
26+
27+
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
28+
pub(crate) struct NodeId(NonZero<usize>);
29+
30+
// Allow node indices to index directly into Qubes:
31+
impl ops::Index<NodeId> for Qube {
32+
type Output = Node;
33+
34+
fn index(&self, index: NodeId) -> &Node {
35+
&self.nodes[index.0.get() - 1]
36+
}
37+
}
38+
39+
impl ops::IndexMut<NodeId> for Qube {
40+
fn index_mut(&mut self, index: NodeId) -> &mut Node {
41+
&mut self.nodes[index.0.get() - 1]
42+
}
43+
}
44+
45+
impl ops::Index<StringId> for Qube {
46+
type Output = str;
47+
48+
fn index(&self, index: StringId) -> &str {
49+
&self.strings[index]
50+
}
51+
}
52+
53+
impl NodeId {
54+
pub fn new_infallible(value: NonZero<usize>) -> NodeId {
55+
NodeId(value)
56+
}
57+
pub fn new(value: usize) -> Option<NodeId> {
58+
NonZero::new(value).map(NodeId)
59+
}
60+
}
61+
62+
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
63+
struct StringId(lasso::Spur);
64+
65+
impl ops::Index<StringId> for lasso::Rodeo {
66+
type Output = str;
67+
68+
fn index(&self, index: StringId) -> &str {
69+
&self[index.0]
70+
}
71+
}
72+
73+
#[derive(Debug)]
74+
pub(crate) struct Node {
75+
pub key: StringId,
76+
pub metadata: HashMap<StringId, Vec<String>>,
77+
pub parent: Option<NodeId>, // If not present, it's the root node
78+
pub values: Vec<StringId>,
79+
pub children: HashMap<StringId, Vec<NodeId>>,
80+
}
81+
82+
impl Node {
83+
fn new_root(q: &mut Qube) -> Node {
84+
Node {
85+
key: q.get_or_intern("root"),
86+
metadata: HashMap::new(),
87+
parent: None,
88+
values: vec![],
89+
children: HashMap::new(),
90+
}
91+
}
92+
93+
fn children(&self) -> impl Iterator<Item = &NodeId> {
94+
self.children.values().flatten()
95+
}
96+
97+
fn is_root(&self) -> bool {
98+
self.parent.is_none()
99+
}
100+
101+
/// Because children are stored grouped by key
102+
/// determining the number of children quickly takes a little effort.
103+
/// This is a fast method for the special case of checking if a Node has exactly one child.
104+
/// Returns Ok(NodeId) if there is one child else None
105+
fn has_exactly_one_child(&self) -> Option<NodeId> {
106+
if self.children.len() != 1 {return None}
107+
let Some(value_group) = self.children.values().next() else {return None};
108+
let [node_id] = &value_group.as_slice() else {return None};
109+
Some(*node_id)
110+
}
111+
112+
fn n_children(&self) -> usize {
113+
self.children
114+
.values()
115+
.map(|v| v.len())
116+
.sum()
117+
}
118+
}
119+
120+
#[derive(Debug)]
121+
#[pyclass(subclass, dict)]
122+
pub struct Qube {
123+
pub root: NodeId,
124+
nodes: Vec<Node>,
125+
strings: Rodeo,
126+
}
127+
128+
impl Qube {
129+
pub fn new() -> Self {
130+
let mut q = Self {
131+
root: NodeId::new(1).unwrap(),
132+
nodes: Vec::new(),
133+
strings: Rodeo::default(),
134+
};
135+
136+
let root = Node::new_root(&mut q);
137+
q.nodes.push(root);
138+
q
139+
}
140+
141+
fn get_or_intern(&mut self, val: &str) -> StringId {
142+
StringId(self.strings.get_or_intern(val))
143+
}
144+
145+
pub fn add_node(&mut self, parent: NodeId, key: &str, values: &[&str]) -> NodeId {
146+
let key_id = self.get_or_intern(key);
147+
let values = values.iter().map(|val| self.get_or_intern(val)).collect();
6148

7-
mod qube;
8-
mod json;
149+
// Create the node object
150+
let node = Node {
151+
key: key_id,
152+
metadata: HashMap::new(),
153+
values: values,
154+
parent: Some(parent),
155+
children: HashMap::new(),
156+
};
157+
158+
// Insert it into the Qube arena and determine its id
159+
self.nodes.push(node);
160+
let node_id = NodeId::new(self.nodes.len()).unwrap();
161+
162+
// Add a reference to this node's id to the parents list of children.
163+
let parent_node = &mut self[parent];
164+
let key_group = parent_node.children.entry(key_id).or_insert(Vec::new());
165+
key_group.push(node_id);
166+
167+
node_id
168+
}
169+
170+
fn print(&self, node_id: Option<NodeId>) -> String {
171+
let node_id: NodeId = node_id.unwrap_or(self.root);
172+
let node = &self[node_id];
173+
node.summary(&self)
174+
}
175+
}
9176

10177

11178
#[pymodule]
12179
fn rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
13-
m.add_class::<qube::Qube>()?;
14-
m.add_function(wrap_pyfunction!(json::parse_qube, m)?);
180+
m.add_class::<Qube>()?;
15181
Ok(())
16182
}

0 commit comments

Comments
 (0)