use std::collections::hash_map::DefaultHasher;
use std::fmt::Write as FormatWrite;
use std::hash::{Hash, Hasher};
use std::io::Write;
use std::ops::Add;
use std::path::{Path, PathBuf};
use std::process::Command;
use log::{debug, info};
use serde_json::Value;
use simpath::{FileType, FoundType, Simpath};
use wax::Glob;
use flowcore::model::connection::Connection;
use flowcore::model::flow_definition::FlowDefinition;
use flowcore::model::function_definition::FunctionDefinition;
use flowcore::model::input::InputInitializer::{Always, Once};
use flowcore::model::io::IOSet;
use flowcore::model::name::HasName;
use flowcore::model::process::Process::{FlowProcess, FunctionProcess};
use flowcore::model::route::{HasRoute, Route};
use flowcore::provider::Provider;
use crate::dumper::create_output_file;
use crate::errors::*;
pub(crate) static INPUT_PORTS: &[&str] = &["n", "ne", "nw", "w"];
pub(crate) static OUTPUT_PORTS: &[&str] = &["s", "se", "sw", "e"];
pub fn dump_flow(
flow: &FlowDefinition,
output_dir: &Path,
provider: &dyn Provider,
) -> Result<()> {
info!(
"\n=== Dumper: Dumping flow hierarchy to '{}'",
output_dir.display()
);
_dump_flow(flow, 0, output_dir, provider)?;
Ok(())
}
pub fn generate_svgs(root_dir: &Path, delete_dots: bool) -> Result<()> {
if let Ok(FoundType::File(dot)) = Simpath::new("PATH").find_type("dot", FileType::File) {
info!("\n=== Dumper: Generating .dot.svg files from .dot files, using 'dot' command from $PATH");
let glob = Glob::new("**/*.dot").map_err(|_| "Globbing error")?;
for entry in glob.walk(root_dir) {
let entry = entry?;
let path = entry.path();
let path_name = path.to_string_lossy();
let mut output_file = path.to_path_buf();
output_file.set_extension("dot.svg");
#[allow(clippy::needless_borrow)]
if Command::new(&dot)
.args(vec!["-Tsvg", &format!("-o{}", output_file.display()), &path_name])
.status()?.success() {
debug!(".dot.svg successfully generated from {path_name}");
if delete_dots {
debug!("Source file {path_name} was removed after SVG generation")
}
} else {
bail!("Error executing 'dot'");
}
}
} else {
info!("Could not find 'dot' command in $PATH so SVG generation skipped");
}
Ok(())
}
#[allow(clippy::only_used_in_recursion)]
fn _dump_flow(
flow: &FlowDefinition,
level: usize,
target_dir: &Path,
provider: &dyn Provider
) -> Result<()> {
let file_path = flow.source_url.to_file_path()
.map_err(|_| "Could not get file_stem of flow definition filename")?;
let filename = file_path
.file_stem()
.ok_or("Could not get file_stem of flow definition filename")?
.to_str()
.ok_or("Could not convert filename to string")?;
let mut writer = create_output_file(target_dir, filename, "dot")?;
info!("\tGenerating {}.dot, Use \"dotty\" to view it", filename);
write_flow_to_dot(flow, &mut writer)?;
for subprocess in &flow.subprocesses {
if let FlowProcess(ref subflow) = subprocess.1 {
_dump_flow(
subflow,
level + 1,
target_dir,
provider
)?;
}
}
Ok(())
}
fn write_flow_to_dot(
flow: &FlowDefinition,
dot_file: &mut dyn Write,
) -> Result<()> {
dot_file.write_all(digraph_start(flow).as_bytes())?;
let mut contents = String::new();
contents.push_str(&input_set_to_dot(flow.inputs(), flow.route()));
contents.push_str(&output_set_to_dot(flow.outputs(), flow.route(), false));
contents.push_str(&process_references_to_dot(flow)?);
contents.push_str("\n\t// Connections");
for connection in &flow.connections {
contents.push_str(&connection_to_dot(connection));
}
dot_file.write_all(contents.as_bytes())?;
dot_file.write_all(digraph_end().as_bytes())?;
Ok(())
}
fn input_set_to_dot(input_set: &IOSet, to: &Route) -> String {
let mut string = "\n\t// Inputs\n\t{ rank=source\n".to_string();
for input in input_set {
if input.route() != to {
let _ = writeln!(string,
"\t\"{}\" [label=\"{}\", shape=house, style=filled, fillcolor=white];",
input.route(),
input.name()
);
}
}
string.push_str("\t}\n");
string
}
fn output_set_to_dot(output_set: &IOSet, from: &Route, connect_subflow: bool) -> String {
let mut string = String::new();
string.push_str("\n\t// Outputs\n\t{ rank=sink\n");
for output in output_set {
if output.route() != from {
let _ = writeln!(string, "\t\"{}\" [label=\"{}\", shape=invhouse, style=filled, fillcolor=black, fontcolor=white];",
output.route(), output.name());
if connect_subflow {
let output_port = output_name_to_port(output.name());
let _ = writeln!(string,
"\t\"{}\":{} -> \"{}\"[style=invis, headtooltip=\"{}\"];",
from,
output_port,
output.route(),
output.name()
);
}
}
}
string.push_str("\t}\n");
string
}
fn process_references_to_dot(flow: &FlowDefinition) -> Result<String> {
let mut contents = "\n\t// Process References\n".to_string();
let file_path = flow.source_url.to_file_path().map_err(|_| "Could not convert Url to file path")?;
for process_ref in &flow.process_refs {
let process = flow.subprocesses.get(process_ref.alias())
.ok_or("Could not find process named in process_ref")?;
match process {
FlowProcess(ref subflow) =>
contents.push_str(&subflow_to_dot(subflow, file_path.clone(),
subflow.route())?),
FunctionProcess(ref function) =>
contents.push_str(&subfunction_to_dot(function, file_path.clone())?),
}
}
Ok(contents)
}
fn subflow_to_dot(flow: &FlowDefinition, parent: PathBuf, flow_route: &str) -> Result<String> {
let flow_source_path = flow.source_url.to_file_path()
.map_err(|_| "Could not convert flow's source_url to a File Path")?;
let relative_path = absolute_to_relative(&flow_source_path, parent)?;
Ok(format!("\t\"{}\" [label=\"{}\", style=filled, fillcolor=aquamarine, width=2, height=2, URL=\"{relative_path}.dot.svg\"];\n",
flow_route, flow.alias))
}
fn subfunction_to_dot(function: &FunctionDefinition, parent: PathBuf) -> Result<String> {
let mut dot_string = String::new();
let name = if function.name() == function.alias() {
"".to_string()
} else {
format!("\\n({})", function.name())
};
let function_source_path = function.get_source_url().to_file_path()
.map_err(|_| "Could not convert function's source_url to a File Path")?;
let relative_path = absolute_to_relative(&function_source_path, parent)?;
let md_path = relative_path.replace("toml", "html");
if function.is_impure() {
if function.inputs.is_empty() { let _ = writeln!(dot_string,
"\t{{ rank=source \n\t\"{}\"[URL=\"{}\", label=\"{}{}\", shape=invhouse, style=filled, fillcolor=white];\n\t}}",
function.route(),
md_path,
function.alias(),
name);
} else { let _ = writeln!(dot_string,
"\t{{ rank=sink \n\t\"{}\"[URL=\"{}\", label=\"{}{}\", shape=house, style=filled, fillcolor=black, fontcolor=white];\n\t}}",
function.route(),
md_path,
function.alias(),
name);
}
} else {
let _ = writeln!(dot_string,
"\t\"{}\" [style=filled, fillcolor=coral, URL=\"{}\", label=\"{}{}\"];",
function.route(),
md_path,
function.alias(),
name);
}
dot_string.push_str(&input_initializers_to_dot(function, function.route().as_ref()));
Ok(dot_string)
}
pub (crate) fn input_initializers_to_dot(function: &FunctionDefinition, function_identifier: &str) -> String {
let mut initializers = "\n\t// Initializers\n".to_string();
for (input_number, input) in function.get_inputs().iter().enumerate() {
if let Some(initializer) = input.get_initializer() {
let (value, line_style) = match initializer {
Always(value) => (value.clone(), "solid"),
Once(value) => (value.clone(), "dotted"),
};
let value_string = if let Value::String(value_str) = value {
format!("\\\"{value_str}\\\"")
} else {
format!("{value}")
};
let _ = writeln!(initializers,
"\t\"initializer{function_identifier}_{input_number}\" [style=invis];"
);
let input_port = input_name_to_port(input.name());
let _ = writeln!(initializers,
"\t\"initializer{function_identifier}_{input_number}\" -> \"{function_identifier}\":{input_port} [style={line_style}] [taillabel=\"{value_string}\"] [len=0.1] [color=blue];");
}
}
initializers
}
fn connection_to_dot(connection: &Connection) -> String {
let (from_route, number, array_index) =
connection.from_io().route().without_trailing_array_index();
let (from_port, from_name, from_node) = if connection.from_io().flow_io() {
("s",
"", from_route.to_string())
} else {
(output_name_to_port(connection.from_io().name()),
connection.from_io().name().as_str(),
strip_io_name(&from_route, connection.from_io().name().as_str()))
};
let (to_port, to_name, to_node) = if connection.to_io().flow_io() {
("n",
"", connection.to_io().route().to_string()
)
} else {
(input_name_to_port(connection.to_io().name()),
connection.to_io().name().as_str(),
strip_io_name(connection.to_io().route(), connection.to_io().name().as_str())
)
};
if array_index {
format!(
"\n\t\"{from_node}\":{from_port} -> \"{to_node}\":{to_port} [xlabel=\"{from_name}[{number}]\", headlabel=\"{to_name}\"];")
} else {
format!(
"\n\t\"{from_node}\":{from_port} -> \"{to_node}\":{to_port} [xlabel=\"{from_name}\", headlabel=\"{to_name}\"];")
}
}
fn digraph_start(flow: &FlowDefinition) -> String {
let mut wrapper = String::new();
let _ = writeln!(wrapper,
"digraph {} {{",
str::replace(&flow.alias.to_string(), "-", "_")
);
let _ = writeln!(wrapper, "\tlabel=\"{}\";", flow.alias);
let _ = writeln!(wrapper, "\tlabelloc=t;");
let _ = writeln!(wrapper, "\tmargin=0.4;");
wrapper
}
fn digraph_end() -> String {
"
} // close digraph\n"
.to_string()
}
fn index_from_name<T: Hash>(t: &T, length: usize) -> usize {
let mut s = DefaultHasher::new();
t.hash(&mut s);
let index = s.finish() % length as u64;
index as usize
}
fn input_name_to_port<T: Hash>(t: &T) -> &str {
INPUT_PORTS[index_from_name(t, INPUT_PORTS.len())]
}
pub(crate) fn output_name_to_port<T: Hash>(t: &T) -> &str {
OUTPUT_PORTS[index_from_name(t, OUTPUT_PORTS.len())]
}
fn strip_io_name(route: &Route, name: &str) -> String {
route.to_string().strip_suffix(&format!("/{name}")).unwrap_or(route).to_string()
}
fn absolute_to_relative(target: &Path, source: PathBuf) -> Result<String> {
let mut current_path = source.parent()
.ok_or("Could not get directory containing source")?.to_path_buf();
let mut relative_path_to_root = String::new();
while !target.starts_with(¤t_path) {
relative_path_to_root.push_str("../");
if !current_path.pop() {
bail!("Could not find a common directory to calculate a relative path")
}
}
let sub_path_from_common_point = target.strip_prefix(current_path.as_path())
.map_err(|_| "Could not calculate sub-path")?;
relative_path_to_root = relative_path_to_root
.add(&sub_path_from_common_point.to_string_lossy());
Ok(relative_path_to_root)
}
#[cfg(test)]
mod test {
use std::path::Path;
use url::Url;
use crate::dumper::flow_to_dot::absolute_to_relative;
#[test]
fn sub_dir_relative_path() {
let target = Path::new("/Users/andrew/workspace/flow/target/flowsamples/mandlebrot/escapes/escapes.html");
let parent = Path::new("/Users/andrew/workspace/flow/target/flowsamples/mandlebrot/render.dot.svg");
let relative = absolute_to_relative(target, parent.to_path_buf())
.expect("Could not form a relative path");
assert_eq!(relative, "escapes/escapes.html");
}
#[test]
fn sub_dir_mixed_schemes_relative_path() {
let target_url = Url::parse("file:///Users/andrew/workspace/flow/target/flowsamples/mandlebrot/escapes/escapes.html")
.expect("Could not parse Url");
let target = target_url.to_file_path().expect("Could not convert to file path");
let parent = Path::new("/Users/andrew/workspace/flow/target/flowsamples/mandlebrot/render.dot.svg");
let relative = absolute_to_relative(&target, parent.to_path_buf())
.expect("Could not form a relative path");
assert_eq!(relative, "escapes/escapes.html");
}
#[test]
fn other_branch_relative_path() {
let target = Path::new("file:///Users/andrew/workspace/flow/target/flowstdlib/control/index_f.html");
let parent = Path::new("file:///Users/andrew/workspace/flow/target/flowsamples/mandlebrot/render.dot.svg");
let relative = absolute_to_relative(target, parent.to_path_buf())
.expect("Could not form a relative path");
assert_eq!(relative, "../../flowstdlib/control/index_f.html");
}
}