diff --git a/levels/game-config.toml b/levels/game-config.toml index 0e92a65..c17a1bd 100644 --- a/levels/game-config.toml +++ b/levels/game-config.toml @@ -2,7 +2,7 @@ title = "clone" branch = "master" solution_checker = "echo No pushing to master. Read the README file; exit 1" - flags = [] # ["start-here"], but it's implicit in the readme + flags = ["start-here"] [[levels]] title = "start-here" diff --git a/scripts/generate-pre-receive-hook/.gitignore b/scripts/.gitignore similarity index 100% rename from scripts/generate-pre-receive-hook/.gitignore rename to scripts/.gitignore diff --git a/scripts/generate-pre-receive-hook/Cargo.toml b/scripts/Cargo.toml similarity index 75% rename from scripts/generate-pre-receive-hook/Cargo.toml rename to scripts/Cargo.toml index 7e1c9db..d2ac915 100644 --- a/scripts/generate-pre-receive-hook/Cargo.toml +++ b/scripts/Cargo.toml @@ -1,16 +1,21 @@ [package] -name = "generate-pre-receive-hook" +name = "make-git-better-scripts" version = "0.1.0" authors = ["Shay Nehmad "] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "common" +path = "src/lib/lib.rs" + [dependencies] structopt = "0.3.13" serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" toml = "0.5" tinytemplate = "1.0.4" simple_logger = "1.6.0" log = "0.4" - +petgraph = "0.5" diff --git a/scripts/src/bin/generate-levels-graph.rs b/scripts/src/bin/generate-levels-graph.rs new file mode 100644 index 0000000..51af246 --- /dev/null +++ b/scripts/src/bin/generate-levels-graph.rs @@ -0,0 +1,163 @@ +use log::{debug, info}; +use petgraph::dot::{Config, Dot}; +use petgraph::graph::NodeIndex; +use petgraph::{Directed, Graph}; +use serde::Serialize; +use std::fs; +use std::io::Write; +use structopt::StructOpt; +use tinytemplate::TinyTemplate; + +use common::{GameConfig, Level}; + +type LevelsGraph = Graph; + +#[derive(Debug, StructOpt)] +#[structopt(about = "A script to generate a levels graph from a game config.")] +struct Cli { + #[structopt(parse(from_os_str), help = "Path to game config file to read")] + game_config_path: std::path::PathBuf, + + #[structopt(parse(from_os_str), help = "Path to the graph template file to read")] + template_path: std::path::PathBuf, + + #[structopt( + parse(from_os_str), + default_value = "output/levelgraph.html", + help = "Path to output file (creates if doesn't exist)" + )] + output_path: std::path::PathBuf, + + #[structopt( + short = "v", + long = "verbose", + help = "Show more information about the actions taken" + )] + verbose: bool, +} + +/// Recursive function that populates the game graph +/// +/// If receives a graph initialized with the first level as a root node. +fn add_level_nodes_to_graph<'a>( + current_level: Level, + current_node: &'a NodeIndex, + levels_graph: &'a mut LevelsGraph, + game_config: &'a GameConfig, +) { + if current_level.flags.len() == 0 { + return; + }; + + for flag in current_level.flags { + debug!("level {} flag {}", current_level.title, flag); + let mut levels_iterator = game_config.levels.iter(); + let found = levels_iterator.find(|x| x.title == flag); + match found { + Some(x) => { + debug!( + "The flag does point to another level, {}. Adding level as node to graph", + x.title + ); + let new_node = levels_graph.add_node(x.clone()); + debug!("Adding edge from {} to {}", current_level.title, x.title); + levels_graph.add_edge(*current_node, new_node, 0); + debug!("Recursive calling add nodes on {}", x.title); + add_level_nodes_to_graph(x.clone(), &new_node, levels_graph, &game_config); + } + None => { + debug!("The flag doesn't point to another level - no need to recurse"); + } + } + } +} + +fn create_graph_from_game_config(game_config: &GameConfig) -> LevelsGraph { + let mut levels_graph = LevelsGraph::new(); + + let first_level = game_config.levels[0].clone(); + let tree_root = levels_graph.add_node(first_level.clone()); + add_level_nodes_to_graph(first_level, &tree_root, &mut levels_graph, &game_config); + + levels_graph +} + +#[cfg(test)] +mod tests { + use super::*; + use petgraph::algo::is_cyclic_directed; + + #[test] + fn test_create_graph_from_game_config() { + let first_level = Level { + title: String::from("first"), + branch: String::from("first"), + solution_checker: String::from("first"), + flags: vec!["second".to_string()], + }; + let second_level = Level { + title: String::from("second"), + branch: String::from("sec"), + solution_checker: String::from("sec"), + flags: vec!["another".to_string(), "asdf".to_string()], + }; + + let game_conf = GameConfig { + levels: vec![first_level, second_level], + }; + let graph = create_graph_from_game_config(&game_conf); + + assert_eq!(graph.node_count(), 2); + assert_eq!(graph.edge_count(), 1); + assert!(graph.is_directed()); + assert!(!is_cyclic_directed(&graph)); + } +} + +#[derive(Serialize)] +struct Context { + levels_graph_as_dot: String, +} + +fn main() { + let args = Cli::from_args(); + + if args.verbose { + simple_logger::init_with_level(log::Level::Debug).unwrap(); + } else { + simple_logger::init_with_level(log::Level::Info).unwrap(); + }; + + info!("Reading script from {:?}", args.game_config_path); + let game_config_file_contents = fs::read_to_string(args.game_config_path).unwrap(); + let game_config: GameConfig = toml::from_str(&game_config_file_contents).unwrap(); + + let levels_graph = create_graph_from_game_config(&game_config); + + let levels_graph_as_dot = Dot::with_config(&levels_graph, &[Config::EdgeNoLabel]); + let context = Context { + levels_graph_as_dot: format!("{}", levels_graph_as_dot), + }; + + debug!("Generated graph:\n{:?}", levels_graph_as_dot); + + info!("Reading template from {:?}", args.template_path); + let template_file_contents = fs::read_to_string(args.template_path).unwrap(); + + let mut tt = TinyTemplate::new(); + let template_name = "levels_graph"; + tt.add_template(template_name, &template_file_contents) + .unwrap(); + let rendered = tt.render(template_name, &context).unwrap(); + + debug!("########## RENDERED TEMPLATE ##########"); + debug!("{}\n", rendered); + + let mut output_dir = args.output_path.clone(); + output_dir.pop(); + fs::create_dir_all(&output_dir).expect("Failed to create parent dir"); + let mut output_file = fs::File::create(&args.output_path).expect("Couldn't create file!"); + output_file.write_all(&rendered.as_bytes()).unwrap(); + + info!("Wrote rendered file to {:?}", args.output_path); +} diff --git a/scripts/generate-pre-receive-hook/src/main.rs b/scripts/src/bin/generate-pre-receive-hook.rs similarity index 77% rename from scripts/generate-pre-receive-hook/src/main.rs rename to scripts/src/bin/generate-pre-receive-hook.rs index f2034e1..61d7fcf 100644 --- a/scripts/generate-pre-receive-hook/src/main.rs +++ b/scripts/src/bin/generate-pre-receive-hook.rs @@ -1,6 +1,4 @@ -use log; use log::{debug, info}; -use serde::{Deserialize, Serialize}; use simple_logger; use std::fs; use std::io::Write; @@ -8,6 +6,8 @@ use structopt::StructOpt; use tinytemplate::TinyTemplate; use toml; +use common::GameConfig; + #[derive(Debug, StructOpt)] #[structopt(about = "A script to generate the master pre-receive hook file.")] struct Cli { @@ -32,19 +32,6 @@ struct Cli { verbose: bool, } -#[derive(Debug, Clone, Deserialize, Serialize)] -struct Level { - title: String, - branch: String, - solution_checker: String, - flags: Vec, -} - -#[derive(Debug, Deserialize, Serialize)] -struct GameConfig { - levels: Vec, -} - fn replace_flags_with_branch_names(game_config: &mut GameConfig) { let levels_info = game_config.levels.clone(); @@ -69,6 +56,36 @@ fn replace_flags_with_branch_names(game_config: &mut GameConfig) { } } +#[cfg(test)] +mod tests { + use super::*; + use common::{GameConfig, Level}; + + #[test] + fn test_replace_flags_with_branch_names() { + let first_level = Level { + title: "a".to_string(), + branch: "a".to_string(), + solution_checker: "a".to_string(), + flags: vec!["second_level_title".to_string()], + }; + let second_level = Level { + title: "second_level_title".to_string(), + branch: "second_level_branch".to_string(), + solution_checker: "b".to_string(), + flags: vec!["c".to_string()], + }; + let mut game_conf = GameConfig { + levels: vec![first_level, second_level], + }; + replace_flags_with_branch_names(&mut game_conf); + assert_eq!( + game_conf.levels[0].flags[0], + "second_level_branch".to_string() + ); + } +} + fn main() { let args = Cli::from_args(); diff --git a/scripts/src/bin/templates/graph.tmpl b/scripts/src/bin/templates/graph.tmpl new file mode 100644 index 0000000..5cbcf91 --- /dev/null +++ b/scripts/src/bin/templates/graph.tmpl @@ -0,0 +1,52 @@ +
+ + diff --git a/scripts/generate-pre-receive-hook/template.tmpl b/scripts/src/bin/templates/hook.tmpl similarity index 100% rename from scripts/generate-pre-receive-hook/template.tmpl rename to scripts/src/bin/templates/hook.tmpl diff --git a/scripts/src/lib/lib.rs b/scripts/src/lib/lib.rs new file mode 100644 index 0000000..16406df --- /dev/null +++ b/scripts/src/lib/lib.rs @@ -0,0 +1,42 @@ +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct Level { + pub title: String, + pub branch: String, + pub solution_checker: String, + pub flags: Vec, +} + +impl fmt::Display for Level { + // This trait requires `fmt` with this exact signature. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Write strictly the first element into the supplied output + // stream: `f`. Returns `fmt::Result` which indicates whether the + // operation succeeded or failed. Note that `write!` uses syntax which + // is very similar to `println!`. + write!(f, "{}", self.title) + } +} + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct GameConfig { + pub levels: Vec, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_level_display() { + let level = Level { + title: "tit".to_string(), + branch: "bra".to_string(), + solution_checker: "sol".to_string(), + flags: vec!["fla".to_string()], + }; + assert_eq!(format!("{}", level), "tit".to_string()); + } +}