diff --git a/README.md b/README.md index 07ddc2c..3d55efe 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,38 @@ For example: -- exists: my_schema.c ``` +### `layer` + +You can organize files into layers that enforce ordering constraints. Files in lower-index layers cannot depend on files in higher-index layers. + +For example: + +```postgresql +-- name: my_schema.setup +-- layer: first + +-- name: my_schema.functions +-- layer: second +-- requires: my_schema.setup + +-- name: my_schema.views +-- layer: third +-- requires: my_schema.functions +``` + +#### Layer Configuration + +- Use `--layers first,second,third` to define custom layers in order +- Use `--fallback-layer second` to specify the default layer for files without explicit layer declarations +- Default layers are `prepend,normal,append` with `normal` as the fallback + +#### Backward Compatibility + +The legacy `-- is_initial` and `-- is_final` headers are still supported: +- `-- is_initial` maps to the "prepend" layer +- `-- is_final` maps to the "append" layer +- Files without layer declarations use the fallback layer + ## Example Lets say you have a directory with the following files: diff --git a/src/config.rs b/src/config.rs index 935db13..f977f11 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,4 +16,6 @@ pub struct Config<'a> { pub exclude_node_prefixes: Option<&'a [String]>, pub include_hidden: bool, pub subdir_filter: Option, + pub layers: Vec, + pub fallback_layer: String, } diff --git a/src/exceptions.rs b/src/exceptions.rs index d21a5da..21950c1 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -21,11 +21,27 @@ impl fmt::Display for TopCatError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::GraphMissing => write!(f, "Graph is None"), - Self::InvalidFileHeader(x, s) => write!(f, "Invalid file header in {}: {}", x.display(), s), - Self::NameClash(name, f1, f2) => write!(f, "Name {} found in both {} and {}", name, f1.display(), f2.display()), - Self::MissingExist(x, s) => write!(f, "MissingExist: {} expects {} to exist but it is not found", x, s), - Self::MissingDependency(x, s) => write!(f, "MissingDependency: {} depends on {} but it is missing", x, s), - Self::InvalidDependency(x, s) => write!(f, "InvalidDependency: {} is marked as prepend so it cannot depend on {} which isn't marked as prepend", s, x), + Self::InvalidFileHeader(x, s) => { + write!(f, "Invalid file header in {}: {}", x.display(), s) + } + Self::NameClash(name, f1, f2) => write!( + f, + "Name {} found in both {} and {}", + name, + f1.display(), + f2.display() + ), + Self::MissingExist(x, s) => write!( + f, + "MissingExist: {} expects {} to exist but it is not found", + x, s + ), + Self::MissingDependency(x, s) => write!( + f, + "MissingDependency: {} depends on {} but it is missing", + x, s + ), + Self::InvalidDependency(x, s) => write!(f, "InvalidDependency: {}: {}", x, s), Self::CyclicDependency(x) => { let mut error_message = "Cyclic dependency detected:\n".to_string(); for (i, cycle) in x.iter().enumerate() { @@ -42,16 +58,13 @@ impl fmt::Display for TopCatError { error_message.push_str(" Edges:\n"); for (i, node) in cycle.iter().enumerate() { let next_node = &cycle[(i + 1) % cycle.len()]; - error_message.push_str(&format!( - " - {} -> {}\n", - node.name, - next_node.name - )); + error_message + .push_str(&format!(" - {} -> {}\n", node.name, next_node.name)); } } write!(f, "{}", error_message) - }, + } Self::Io(err) => write!(f, "IO error: {}", err), Self::UnknownError(s) => write!(f, "UnknownError: {}", s), } @@ -70,6 +83,7 @@ impl Error for TopCatError {} pub enum FileNodeError { TooManyNames(PathBuf, Vec), NoNameDefined(PathBuf), + InvalidLayer(PathBuf, String), } impl fmt::Display for FileNodeError { @@ -82,6 +96,9 @@ impl fmt::Display for FileNodeError { s.join(", ") ), Self::NoNameDefined(x) => write!(f, "No name defined in {}", x.display()), + Self::InvalidLayer(x, layer) => { + write!(f, "Invalid layer '{}' declared in {}", layer, x.display()) + } } } } diff --git a/src/file_dag.rs b/src/file_dag.rs index 20964d9..0514f12 100644 --- a/src/file_dag.rs +++ b/src/file_dag.rs @@ -15,31 +15,6 @@ use crate::file_node::FileNode; use crate::stable_topo::StableTopo; use crate::{config, io_utils}; -/// The `TCGraphType` enum represents the different types of graph modifications. -/// -/// These modifications can be applied to a graph to manipulate its content. -/// -/// # Variants -/// -/// - `Normal`: Indicates no modifications will be applied to the graph. -/// - `Prepend`: Indicates that new elements will be prepended to the graph. -/// - `Append`: Indicates that new elements will be appended to the graph. -pub enum TCGraphType { - Normal, - Prepend, - Append, -} - -impl TCGraphType { - pub fn as_str(&self) -> &str { - match self { - TCGraphType::Normal => "normal", - TCGraphType::Prepend => "prepend", - TCGraphType::Append => "append", - } - } -} - fn string_slice_to_array(option: Option<&[T]>) -> Option> { match option { Some(arr) => Some(arr.iter().cloned().collect()), @@ -130,42 +105,45 @@ fn handle_file_node_error(e: FileNodeError) -> Result<(), TopCatError> { p, format!("Too many names declared: {}", s.join(", ")), )), + FileNodeError::InvalidLayer(p, layer) => Err(TopCatError::InvalidFileHeader( + p, + format!("Invalid layer '{}' declared", layer), + )), }; } fn add_nodes_to_graphs( - prepend_graph: &mut Graph, - append_graph: &mut Graph, - normal_graph: &mut Graph, - prepend_index_map: &mut HashMap, - append_index_map: &mut HashMap, - normal_index_map: &mut HashMap, + layer_graphs: &mut HashMap>, + layer_index_maps: &mut HashMap>, name_map: &HashMap, ) { for file_node in name_map.values() { - let idx: NodeIndex; - if file_node.prepend { - idx = prepend_graph.add_node(file_node.clone()); - prepend_index_map.insert(file_node.name.clone(), idx); - } else if file_node.append { - idx = append_graph.add_node(file_node.clone()); - append_index_map.insert(file_node.name.clone(), idx); - } else { - idx = normal_graph.add_node(file_node.clone()); - normal_index_map.insert(file_node.name.clone(), idx); - } + let layer = &file_node.layer; + let graph = layer_graphs + .get_mut(layer) + .expect("Layer graph should exist"); + let index_map = layer_index_maps + .get_mut(layer) + .expect("Layer index map should exist"); + + let idx = graph.add_node(file_node.clone()); + index_map.insert(file_node.name.clone(), idx); } } fn validate_dependencies( name_map: &HashMap, - prepend_graph: &mut Graph, - append_graph: &mut Graph, - normal_graph: &mut Graph, - prepend_index_map: &HashMap, - append_index_map: &HashMap, - normal_index_map: &HashMap, + layer_graphs: &mut HashMap>, + layer_index_maps: &HashMap>, + layers: &[String], ) -> Result<(), TopCatError> { + // Create a map from layer name to its index for dependency validation + let layer_indices: HashMap = layers + .iter() + .enumerate() + .map(|(i, layer)| (layer.clone(), i)) + .collect(); + for file_node in name_map.values() { for ensure in &file_node.ensure_exists { if !name_map.contains_key(ensure) { @@ -181,39 +159,29 @@ fn validate_dependencies( TopCatError::MissingDependency(file_node.name.clone(), dep.clone()) })?; - if file_node.prepend { - if !dep_node.prepend { - return Err(TopCatError::InvalidDependency( - file_node.name.clone(), - dep.clone(), - )); - } - prepend_graph.add_edge( - *prepend_index_map.get(dep).unwrap(), - *prepend_index_map.get(&file_node.name).unwrap(), + let file_layer_idx = layer_indices.get(&file_node.layer).unwrap(); + let dep_layer_idx = layer_indices.get(&dep_node.layer).unwrap(); + + // Enforce layer ordering: lower index layers cannot depend on higher index layers + if file_layer_idx < dep_layer_idx { + return Err(TopCatError::InvalidDependency( + file_node.name.clone(), + format!( + "Node in layer '{}' (index {}) cannot depend on node '{}' in layer '{}' (index {})", + file_node.layer, file_layer_idx, dep.clone(), dep_node.layer, dep_layer_idx + ), + )); + } + + // Only add edges within the same layer + if file_node.layer == dep_node.layer { + let graph = layer_graphs.get_mut(&file_node.layer).unwrap(); + let index_map = layer_index_maps.get(&file_node.layer).unwrap(); + graph.add_edge( + *index_map.get(dep).unwrap(), + *index_map.get(&file_node.name).unwrap(), (), ); - } else if file_node.append { - if dep_node.append { - append_graph.add_edge( - *append_index_map.get(dep).unwrap(), - *append_index_map.get(&file_node.name).unwrap(), - (), - ); - } - } else { - if dep_node.append { - return Err(TopCatError::InvalidDependency( - file_node.name.clone(), - dep.clone(), - )); - } else if !dep_node.prepend { - normal_graph.add_edge( - *normal_index_map.get(dep).unwrap(), - *normal_index_map.get(&file_node.name).unwrap(), - (), - ); - } } } } @@ -240,29 +208,16 @@ fn convert_cycle_indexes_to_cycle_nodes( .collect() } fn check_cyclic_dependencies( - normal_graph: &Graph, - prepend_graph: &Graph, - append_graph: &Graph, + layer_graphs: &HashMap>, ) -> Result<(), TopCatError> { let mut cycles: Vec> = Vec::new(); - if is_cyclic_directed(prepend_graph) { - cycles.extend(convert_cycle_indexes_to_cycle_nodes( - prepend_graph.cycles(), - prepend_graph, - )); - } - if is_cyclic_directed(normal_graph) { - cycles.extend(convert_cycle_indexes_to_cycle_nodes( - normal_graph.cycles(), - normal_graph, - )); - } - if is_cyclic_directed(append_graph) { - cycles.extend(convert_cycle_indexes_to_cycle_nodes( - append_graph.cycles(), - append_graph, - )); + + for graph in layer_graphs.values() { + if is_cyclic_directed(graph) { + cycles.extend(convert_cycle_indexes_to_cycle_nodes(graph.cycles(), graph)); + } } + if !cycles.is_empty() { return Err(TopCatError::CyclicDependency(cycles)); } @@ -279,14 +234,12 @@ pub struct TCGraph { pub exclude_extensions: Option>, pub include_node_prefixes: Option>, pub exclude_node_prefixes: Option>, - normal_graph: DiGraph, - prepend_graph: DiGraph, - append_graph: DiGraph, + layer_graphs: HashMap>, + layer_index_maps: HashMap>, + layers: Vec, + fallback_layer: String, path_map: HashMap, name_map: HashMap, - normal_index_map: HashMap, - prepend_index_map: HashMap, - append_index_map: HashMap, include_hidden: bool, graph_is_built: bool, subdir_filter: Option, @@ -309,6 +262,14 @@ impl TCGraph { let exclude_node_prefixes: Option> = string_slice_to_array(config.exclude_node_prefixes); + // Initialize graphs and index maps for each layer + let mut layer_graphs = HashMap::new(); + let mut layer_index_maps = HashMap::new(); + for layer in &config.layers { + layer_graphs.insert(layer.clone(), DiGraph::new()); + layer_index_maps.insert(layer.clone(), HashMap::new()); + } + TCGraph { comment_str: config.comment_str.clone(), file_dirs: config.input_dirs.clone(), @@ -318,14 +279,12 @@ impl TCGraph { exclude_extensions, include_node_prefixes, exclude_node_prefixes, - normal_graph: DiGraph::new(), - prepend_graph: DiGraph::new(), - append_graph: DiGraph::new(), + layer_graphs, + layer_index_maps, + layers: config.layers.clone(), + fallback_layer: config.fallback_layer.clone(), path_map: HashMap::new(), name_map: HashMap::new(), - normal_index_map: HashMap::new(), - prepend_index_map: HashMap::new(), - append_index_map: HashMap::new(), include_hidden: config.include_hidden, graph_is_built: false, subdir_filter: config.subdir_filter.clone(), @@ -348,7 +307,12 @@ impl TCGraph { ); for file in filtered_files { - let file_node = match FileNode::from_file(&self.comment_str, &file) { + let file_node = match FileNode::from_file( + &self.comment_str, + &file, + &self.layers, + &self.fallback_layer, + ) { Ok(f) => f, Err(e) => { handle_file_node_error(e)?; @@ -370,26 +334,19 @@ impl TCGraph { } add_nodes_to_graphs( - &mut self.prepend_graph, - &mut self.append_graph, - &mut self.normal_graph, - &mut self.prepend_index_map, - &mut self.append_index_map, - &mut self.normal_index_map, + &mut self.layer_graphs, + &mut self.layer_index_maps, &self.name_map, ); validate_dependencies( &self.name_map, - &mut self.prepend_graph, - &mut self.append_graph, - &mut self.normal_graph, - &self.prepend_index_map, - &self.append_index_map, - &self.normal_index_map, + &mut self.layer_graphs, + &self.layer_index_maps, + &self.layers, )?; - check_cyclic_dependencies(&self.normal_graph, &self.prepend_graph, &self.append_graph)?; + check_cyclic_dependencies(&self.layer_graphs)?; self.graph_is_built = true; Ok(()) @@ -428,16 +385,14 @@ impl TCGraph { pub fn graph_as_dot( &self, - graph_type: TCGraphType, + layer_name: &str, ) -> Result>, TopCatError> { if !self.graph_is_built { return Err(TopCatError::GraphMissing); } - let graph = match graph_type { - TCGraphType::Normal => &self.normal_graph, - TCGraphType::Prepend => &self.prepend_graph, - TCGraphType::Append => &self.append_graph, - }; + let graph = self.layer_graphs.get(layer_name).ok_or_else(|| { + TopCatError::UnknownError(format!("Layer '{}' not found", layer_name)) + })?; let dot = Dot::with_attr_getters( graph, &[Config::EdgeNoLabel, Config::NodeNoLabel], @@ -499,22 +454,12 @@ impl TCGraph { let mut sorted_files = Vec::new(); - for graph_type in [ - TCGraphType::Prepend, - TCGraphType::Normal, - TCGraphType::Append, - ] - .iter() - { - let graph = match graph_type { - TCGraphType::Prepend => &self.prepend_graph, - TCGraphType::Normal => &self.normal_graph, - TCGraphType::Append => &self.append_graph, - }; + for layer_name in &self.layers { + let graph = self.layer_graphs.get(layer_name).unwrap(); debug!( "{} graph: {:?} nodes and {:?} edges", - graph_type.as_str(), + layer_name, graph.node_count(), graph.edge_count() ); @@ -525,7 +470,7 @@ impl TCGraph { Some(x) => x, None => return Err(TopCatError::UnknownError("Node not found".to_string())), }; - trace!("{} node: {:?}", graph_type.as_str(), file_node.name); + trace!("{} node: {:?}", layer_name, file_node.name); let mut should_include = true; diff --git a/src/file_node.rs b/src/file_node.rs index 6ed7517..f5c8903 100644 --- a/src/file_node.rs +++ b/src/file_node.rs @@ -42,8 +42,7 @@ pub struct FileNode { pub name: String, pub path: PathBuf, pub deps: HashSet, - pub prepend: bool, - pub append: bool, + pub layer: String, pub ensure_exists: HashSet, } @@ -85,16 +84,14 @@ impl FileNode { name: String, path: PathBuf, deps: HashSet, - prepend: bool, - append: bool, + layer: String, ensure_exists: HashSet, ) -> FileNode { FileNode { name, path, deps, - prepend, - append, + layer, ensure_exists, } } @@ -111,19 +108,25 @@ impl FileNode { }) .collect() } - pub fn from_file(comment_str: &str, path: &PathBuf) -> Result { + pub fn from_file( + comment_str: &str, + path: &PathBuf, + layers: &[String], + fallback_layer: &str, + ) -> Result { let file_data = get_file_headers(&path, comment_str); let name_str = format!("{} name:", comment_str); let dep_str = format!("{} requires:", comment_str); let drop_str = format!("{} dropped_by:", comment_str); + let layer_str = format!("{} layer:", comment_str); + // Keep backward compatibility with old headers let prepend_str = format!("{} is_initial", comment_str); let append_str = format!("{} is_final", comment_str); let ensure_exists_str = format!("{} exists:", comment_str); let mut name = String::new(); let mut deps = HashSet::new(); - let mut is_initial = false; - let mut is_final = false; + let mut layer = fallback_layer.to_string(); let mut ensure_exists = HashSet::new(); for unprocessed_line in &file_data { @@ -149,12 +152,18 @@ impl FileNode { for item in Self::split_dependencies(&line[drop_str.len()..]) { deps.insert(item); } + } else if line.starts_with(&layer_str) { + // -- layer: prepend -> "prepend" + let declared_layer = line[layer_str.len()..].trim(); + if !declared_layer.is_empty() { + layer = declared_layer.to_string(); + } } else if line.starts_with(&prepend_str) { - // -- is_initial -> true - is_initial = true; + // -- is_initial -> "prepend" (backward compatibility) + layer = "prepend".to_string(); } else if line.starts_with(&append_str) { - // -- is_final -> true - is_final = true; + // -- is_final -> "append" (backward compatibility) + layer = "append".to_string(); } else if line.starts_with(&ensure_exists_str) { // --exists: tomato, potato -> ["tomato", "potato"] for item in Self::split_dependencies(&line[ensure_exists_str.len()..]) { @@ -165,13 +174,170 @@ impl FileNode { if name.is_empty() { return Err(FileNodeError::NoNameDefined(path.clone())); } + + // Validate that the declared layer exists in the configured layers + if !layers.contains(&layer) { + return Err(FileNodeError::InvalidLayer(path.clone(), layer)); + } + Ok(FileNode::new( name, path.clone(), deps, - is_initial, - is_final, + layer, ensure_exists, )) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_layer_header_format() { + let layers = vec![ + "first".to_string(), + "second".to_string(), + "third".to_string(), + ]; + let fallback_layer = "second"; + + // Create a temporary file with new layer format + let temp_file = tempfile::NamedTempFile::with_suffix(".sql").unwrap(); + std::fs::write(&temp_file, "-- name: test_node\n-- layer: first\nSELECT 1;").unwrap(); + + let file_node = FileNode::from_file( + "--", + &temp_file.path().to_path_buf(), + &layers, + fallback_layer, + ) + .unwrap(); + + assert_eq!(file_node.name, "test_node"); + assert_eq!(file_node.layer, "first"); + } + + #[test] + fn test_backward_compatibility_is_initial() { + let layers = vec![ + "prepend".to_string(), + "normal".to_string(), + "append".to_string(), + ]; + let fallback_layer = "normal"; + + let temp_file = tempfile::NamedTempFile::with_suffix(".sql").unwrap(); + std::fs::write(&temp_file, "-- name: test_node\n-- is_initial\nSELECT 1;").unwrap(); + + let file_node = FileNode::from_file( + "--", + &temp_file.path().to_path_buf(), + &layers, + fallback_layer, + ) + .unwrap(); + + assert_eq!(file_node.name, "test_node"); + assert_eq!(file_node.layer, "prepend"); + } + + #[test] + fn test_backward_compatibility_is_final() { + let layers = vec![ + "prepend".to_string(), + "normal".to_string(), + "append".to_string(), + ]; + let fallback_layer = "normal"; + + let temp_file = tempfile::NamedTempFile::with_suffix(".sql").unwrap(); + std::fs::write(&temp_file, "-- name: test_node\n-- is_final\nSELECT 1;").unwrap(); + + let file_node = FileNode::from_file( + "--", + &temp_file.path().to_path_buf(), + &layers, + fallback_layer, + ) + .unwrap(); + + assert_eq!(file_node.name, "test_node"); + assert_eq!(file_node.layer, "append"); + } + + #[test] + fn test_fallback_layer() { + let layers = vec![ + "first".to_string(), + "second".to_string(), + "third".to_string(), + ]; + let fallback_layer = "second"; + + let temp_file = tempfile::NamedTempFile::with_suffix(".sql").unwrap(); + std::fs::write(&temp_file, "-- name: test_node\nSELECT 1;").unwrap(); + + let file_node = FileNode::from_file( + "--", + &temp_file.path().to_path_buf(), + &layers, + fallback_layer, + ) + .unwrap(); + + assert_eq!(file_node.name, "test_node"); + assert_eq!(file_node.layer, "second"); + } + + #[test] + fn test_invalid_layer_error() { + let layers = vec!["first".to_string(), "second".to_string()]; + let fallback_layer = "first"; + + let temp_file = tempfile::NamedTempFile::with_suffix(".sql").unwrap(); + std::fs::write( + &temp_file, + "-- name: test_node\n-- layer: invalid\nSELECT 1;", + ) + .unwrap(); + + let result = FileNode::from_file( + "--", + &temp_file.path().to_path_buf(), + &layers, + fallback_layer, + ); + + assert!(result.is_err()); + match result.unwrap_err() { + FileNodeError::InvalidLayer(_, layer) => assert_eq!(layer, "invalid"), + _ => panic!("Expected InvalidLayer error"), + } + } + + #[test] + fn test_dependencies_parsing() { + let layers = vec!["first".to_string(), "second".to_string()]; + let fallback_layer = "first"; + + let temp_file = tempfile::NamedTempFile::with_suffix(".sql").unwrap(); + std::fs::write(&temp_file, "-- name: test_node\n-- layer: first\n-- requires: dep1, dep2\n-- dropped_by: dep3\nSELECT 1;").unwrap(); + + let file_node = FileNode::from_file( + "--", + &temp_file.path().to_path_buf(), + &layers, + fallback_layer, + ) + .unwrap(); + + assert_eq!(file_node.name, "test_node"); + assert_eq!(file_node.layer, "first"); + assert!(file_node.deps.contains("dep1")); + assert!(file_node.deps.contains("dep2")); + assert!(file_node.deps.contains("dep3")); + assert_eq!(file_node.deps.len(), 3); + } +} diff --git a/src/main.rs b/src/main.rs index aa75d06..81aff51 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,6 @@ use structopt::StructOpt; use file_dag::TCGraph; use crate::exceptions::TopCatError; -use crate::file_dag::TCGraphType; mod config; mod exceptions; @@ -128,6 +127,20 @@ struct Opt { help = "Only print the output, do not write to file" )] dry_run: bool, + + #[structopt( + long = "layers", + help = "Comma-separated list of layer names in order", + value_name = "LAYERS" + )] + layers: Option, + + #[structopt( + long = "fallback-layer", + help = "Default layer for nodes without explicit layer declaration", + value_name = "LAYER" + )] + fallback_layer: Option, } fn main() -> Result<(), TopCatError> { let opt = Opt::from_args(); @@ -137,6 +150,32 @@ fn main() -> Result<(), TopCatError> { Builder::new().filter(None, LevelFilter::Info).init(); } + // Parse layers from CLI or use defaults + let layers = if let Some(layers_str) = opt.layers { + layers_str + .split(',') + .map(|s| s.trim().to_string()) + .collect() + } else { + vec![ + "prepend".to_string(), + "normal".to_string(), + "append".to_string(), + ] + }; + + // Set fallback layer + let fallback_layer = opt.fallback_layer.unwrap_or_else(|| "normal".to_string()); + + // Validate that fallback layer exists in layers + if !layers.contains(&fallback_layer) { + eprintln!( + "Error: Fallback layer '{}' is not in the layers list: {:?}", + fallback_layer, layers + ); + std::process::exit(1); + } + let config = config::Config { input_dirs: opt.input_dirs, include_extensions: opt.include_file_extensions.as_deref(), @@ -153,6 +192,8 @@ fn main() -> Result<(), TopCatError> { exclude_node_prefixes: opt.exclude_node_prefixes.as_deref(), dry_run: opt.dry_run, subdir_filter: opt.subdir_filter, + layers, + fallback_layer, }; let mut filedag = TCGraph::new(&config); @@ -168,15 +209,9 @@ fn main() -> Result<(), TopCatError> { } if config.verbose { - println!( - "Prepend Graph: {:#?}", - filedag.graph_as_dot(TCGraphType::Prepend)? - ); - println!("Graph: {:#?}", filedag.graph_as_dot(TCGraphType::Normal)?); - println!( - "Append Graph: {:#?}", - filedag.graph_as_dot(TCGraphType::Append)? - ); + for layer in &config.layers { + println!("{} Graph: {:#?}", layer, filedag.graph_as_dot(layer)?); + } } let result = output::generate(filedag, config, &mut fs::RealFileSystem);