Skip to content

Commit 927a921

Browse files
committed
feat: verify graph before rendering
1 parent 10ba583 commit 927a921

File tree

4 files changed

+90
-43
lines changed

4 files changed

+90
-43
lines changed

README.md

+28
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,34 @@
77
```
88
xattr -d com.apple.quarantine ./layercake
99
```
10+
## Usage
11+
12+
### Example
13+
14+
Using the sample project,
15+
16+
```
17+
# Generate the sample project
18+
$ layercake generate sample kvm_control_flow
19+
INFO layercake::generate_commands: Sample project generated successfully at: "kvm_control_flow"
20+
21+
# Run the sample project with a plan, this will generate the output files
22+
$ layercake run -p kvm_control_flow/plan.yaml
23+
INFO layercake: Running plan: kvm_control_flow/plan.yaml
24+
25+
# Run the sample project with a plan, re-run the plan on input changes
26+
$ layercake run -p kvm_control_flow/plan.yaml -w
27+
```
28+
29+
30+
#### Example linux using inotifywait
31+
32+
```bash
33+
while true; \
34+
do inotifywait -e close_write out/kvm_control_flow.dot && \
35+
dot -Tpng out/kvm_control_flow.dot -o out/kvm_control_flow.png; \
36+
done
37+
```
1038

1139
## Development
1240

sample/attack_tree/plan.yaml

+7-7
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ import:
99

1010
export:
1111
profiles:
12-
- filename: "out/kvm-control-flow.gml"
12+
- filename: "out/attack-tree.gml"
1313
exporter: "GML"
14-
- filename: "out/kvm-control-flow.puml"
14+
- filename: "out/attack-tree.puml"
1515
exporter: "PlantUML"
16-
- filename: "out/kvm-control-flow.mermaid"
16+
- filename: "out/attack-tree.mermaid"
1717
exporter: "Mermaid"
18-
- filename: "out/kvm-control-flow.dot"
18+
- filename: "out/attack-tree.dot"
1919
exporter: "DOT"
20-
- filename: "out/kvm-control-flow-nodes-full.csv"
20+
- filename: "out/attack-tree-nodes-full.csv"
2121
exporter: "CSVNodes"
22-
- filename: "out/kvm-control-flow-edges-full.csv"
22+
- filename: "out/attack-tree-edges-full.csv"
2323
exporter: "CSVEdges"
24-
# - filename: "out/kvm-control-flow-custom.dot"
24+
# - filename: "out/attack-tree-custom.dot"
2525
# exporter: !Custom
2626
# template: "sample/attack_tree/custom.hbs"
2727
# # partials:

src/graph.rs

+29-9
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use polars::frame::row::Row;
22
use regex::Regex;
33
use serde::{Deserialize, Serialize};
44
use std::collections::{HashMap, HashSet};
5-
use tracing::error;
5+
use tracing::{info, warn};
66

77
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
88
pub struct Graph {
@@ -114,24 +114,44 @@ impl Graph {
114114
serde_json::json!(tree)
115115
}
116116

117-
pub fn verify_graph_integrity(&self) -> Result<(), String> {
118-
// TODO verify that all nodes in edges are present in nodes
117+
pub fn verify_graph_integrity(&self) -> Result<(), Vec<String>> {
119118
// TODO verify graph integrity
120-
//
119+
// TODO verify that all nodes have unique ids
120+
121121
let node_ids: HashSet<String> = self.nodes.iter().map(|n| n.id.clone()).collect();
122+
let mut errors = Vec::new();
122123

124+
let mut all_edges_have_nodes = true;
123125
for edge in &self.edges {
124126
if !node_ids.contains(&edge.source) {
125-
let err = format!("Edge source {:?} not found in nodes", edge.source);
126-
error!(err);
127+
all_edges_have_nodes = false;
128+
let err = format!(
129+
"Edge id:[{}] source {:?} not found in nodes",
130+
edge.id, edge.source
131+
);
132+
errors.push(err);
127133
}
128134
if !node_ids.contains(&edge.target) {
129-
let err = format!("Target source {:?} not found in nodes", edge.target);
130-
error!(err);
135+
all_edges_have_nodes = false;
136+
let err = format!(
137+
"Edge id:[{}] target {:?} not found in nodes",
138+
edge.id, edge.target
139+
);
140+
errors.push(err);
131141
}
132142
}
133143

134-
Ok(())
144+
if all_edges_have_nodes {
145+
info!("All edges have valid source and target nodes");
146+
} else {
147+
warn!("Some edges have missing source and/or target nodes");
148+
}
149+
150+
if errors.is_empty() {
151+
Ok(())
152+
} else {
153+
Err(errors)
154+
}
135155
}
136156
}
137157

src/plan_execution.rs

+26-27
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
use crate::data_loader;
22
use crate::graph::{Edge, Graph, Layer, Node};
33
use crate::plan::{ExportFileType, ImportFileType, Plan};
4-
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
4+
use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
55
use std::sync::mpsc::channel;
6-
use std::time::Duration;
7-
use tracing::{debug, error, info};
6+
use tracing::{debug, error, info, warn};
87

98
use anyhow::Result;
109
use polars::prelude::*;
@@ -98,34 +97,34 @@ fn run_plan(plan: Plan, plan_file_path: &std::path::Path) -> Result<()> {
9897

9998
match graph.verify_graph_integrity() {
10099
Ok(_) => {
101-
info!("Graph integrity verified");
100+
info!("Graph integrity verified : ok - rendering exports");
101+
plan.export.profiles.iter().for_each(|profile| {
102+
info!(
103+
"Exporting file: {} using exporter {:?}",
104+
profile.filename, profile.exporter
105+
);
106+
let output = match profile.exporter.clone() {
107+
ExportFileType::GML => super::export::to_gml::render(graph.clone()),
108+
ExportFileType::DOT => super::export::to_dot::render(graph.clone()),
109+
ExportFileType::CSVNodes => "".to_string(),
110+
ExportFileType::CSVEdges => "".to_string(),
111+
ExportFileType::PlantUML => super::export::to_plantuml::render(graph.clone()),
112+
ExportFileType::Mermaid => super::export::to_mermaid::render(graph.clone()),
113+
ExportFileType::Custom(template) => {
114+
super::export::to_custom::render(graph.clone(), template)
115+
}
116+
};
117+
118+
super::common::write_string_to_file(&profile.filename, &output).unwrap();
119+
});
102120
}
103-
Err(e) => {
104-
error!("Error: {}", e);
105-
// return Err(e.into());
121+
Err(errors) => {
122+
warn!("Identified {} graph integrity error(s)", errors.len());
123+
errors.iter().for_each(|e| warn!("{}", e));
124+
warn!("Not rendering exports");
106125
}
107126
}
108127

109-
plan.export.profiles.iter().for_each(|profile| {
110-
info!(
111-
"Exporting file: {} using exporter {:?}",
112-
profile.filename, profile.exporter
113-
);
114-
let output = match profile.exporter.clone() {
115-
ExportFileType::GML => super::export::to_gml::render(graph.clone()),
116-
ExportFileType::DOT => super::export::to_dot::render(graph.clone()),
117-
ExportFileType::CSVNodes => "".to_string(),
118-
ExportFileType::CSVEdges => "".to_string(),
119-
ExportFileType::PlantUML => super::export::to_plantuml::render(graph.clone()),
120-
ExportFileType::Mermaid => super::export::to_mermaid::render(graph.clone()),
121-
ExportFileType::Custom(template) => {
122-
super::export::to_custom::render(graph.clone(), template)
123-
}
124-
};
125-
126-
super::common::write_string_to_file(&profile.filename, &output).unwrap();
127-
});
128-
129128
debug!("Graph: {:?}", graph);
130129

131130
Ok(())

0 commit comments

Comments
 (0)