Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion levels/game-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
[package]
name = "generate-pre-receive-hook"
name = "make-git-better-scripts"
version = "0.1.0"
authors = ["Shay Nehmad <shay.nehmad@guardicore.com>"]
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"
163 changes: 163 additions & 0 deletions scripts/src/bin/generate-levels-graph.rs
Original file line number Diff line number Diff line change
@@ -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<Level, i32, Directed>;

#[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);
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use log;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use simple_logger;
use std::fs;
use std::io::Write;
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 {
Expand All @@ -32,19 +32,6 @@ struct Cli {
verbose: bool,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
struct Level {
title: String,
branch: String,
solution_checker: String,
flags: Vec<String>,
}

#[derive(Debug, Deserialize, Serialize)]
struct GameConfig {
levels: Vec<Level>,
}

fn replace_flags_with_branch_names(game_config: &mut GameConfig) {
let levels_info = game_config.levels.clone();

Expand All @@ -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();

Expand Down
52 changes: 52 additions & 0 deletions scripts/src/bin/templates/graph.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<div id="mynetwork"></div>

<script type="text/javascript">
var DOTstring = `
{levels_graph_as_dot | unescaped}
`;
var parsedData = vis.parseDOTNetwork(DOTstring);

var data = \{
nodes: parsedData.nodes,
edges: parsedData.edges
}

// create a network
var container = document.getElementById('mynetwork');

var options = \{
autoResize: true,
nodes: \{
shape: "box",
shadow: true,
color: "#e8eef2",
font: "20px arial black"
},
edges: \{
color: "#e8eef2",
},
physics: \{
enabled: true,
solver: "hierarchicalRepulsion",
},
layout: \{
hierarchical: \{
direction: "LR",
levelSeparation: 100,
nodeSpacing: 33,
}
}
};

// initialize your network!
var network = new vis.Network(container, data, options);
network.on("click", function(params) \{
if (1 == params.nodes.length) \{
levelName = data.nodes[params.nodes[0]].label;
console.log("Clicked on one node, it's this node: " + levelName);
resulting_url = document.location.origin + "/levels/" + levelName;
console.log("Resulting URL: " + resulting_url);
document.location.href = resulting_url;
}
});
</script>
42 changes: 42 additions & 0 deletions scripts/src/lib/lib.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}

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<Level>,
}

#[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());
}
}