From e35f9b5ac401068537a5da41104f30f209b27cbc Mon Sep 17 00:00:00 2001 From: himanoa Date: Sat, 21 Aug 2021 04:00:03 +0900 Subject: [PATCH 1/2] Implement graph option Fix #12 --- src/lib.rs | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 16 ++++++++ 2 files changed, 120 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 0a8b3b5..f51c23e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,94 @@ pub fn parse_file(path: &PathBuf) -> Result<(), pest::error::Error> { } } +pub fn parse_graph_stages(path: &PathBuf) -> Result<(), PestError> { + use std::fs::File; + use std::io::Read; + + + match File::open(path) { + Ok(mut file) => { + let mut contents = String::new(); + + if let Err(e) = file.read_to_string(&mut contents) { + return Err(PestError::new_from_pos( + ErrorVariant::CustomError { + message: format!("{}", e), + }, + pest::Position::from_start(""), + )); + } else { + println!("{}", build_dot_file(stage_graphs(&contents)?)); + Ok(()) + } + } + Err(e) => { + return Err(PestError::new_from_pos( + ErrorVariant::CustomError { + message: format!("{}", e), + }, + pest::Position::from_start(""), + )); + } + } +} + +fn build_dot_file<'a>(stages: Vec<&str>) -> String { + let nodes = stages.windows(2).fold("".to_string(), |acc, stage| { + let stage1 = stage[0]; + let stage2 = stage[1]; + + if acc == "".to_string() { + return format!("\"{}\" -> \"{}\";", stage1, stage2); + } + format!("{}\"{}\" -> \"{}\";", acc, stage1, stage2) + }); + format!(r#"digraph {{ {} }}"#, nodes) +} + +fn stage_graphs(buffer: &str) -> Result, PestError> { + fn get_stage_names(pairs: Pairs) -> impl Iterator + '_ { + pairs.flat_map(|stage| { + if let Rule::stage = stage.as_rule() { + if let Some(stage_name_span) = stage.into_inner().next() { + let stage_name = stage_name_span.as_str(); + return Some(&stage_name[1..stage_name.len()-1]); + } + return None; + } + return None + }) + } + + if !is_declarative(buffer) { + return Err(PestError::new_from_pos( + ErrorVariant::CustomError { + message: "The buffer does not appear to be a Declarative Pipeline, I couldn't find pipeline { }".to_string(), + }, + pest::Position::from_start(buffer), + )); + } + + let parser = PipelineParser::parse(Rule::pipeline, buffer)?; + if let Some(a) = parser.flat_map(|parsed| { + match parsed.as_rule() { + Rule::stagesDecl => { + Some(get_stage_names(parsed.into_inner())) + } + _ => None + } + }).next() { + return Ok(a.collect::>()); + } else { + return Err(PestError::new_from_pos( + ErrorVariant::CustomError { + message: "I couldn't find stages { }".to_string(), + }, + pest::Position::from_start(buffer), + )); + } +} + pub fn parse_pipeline_string(buffer: &str) -> Result<(), PestError> { if !is_declarative(buffer) { return Err(PestError::new_from_pos( @@ -525,4 +613,20 @@ pipeline { .next() .unwrap(); } + + #[test] + fn test_stage_graphs() { + assert_eq!( + stage_graphs("pipeline{ agent any stages { stage('Build') { steps { sh 'printenv' }} stage('Test') { steps { sh 'cargo test' } } }}").unwrap(), + vec!["Build", "Test"] + ) + } + + #[test] + fn test_build_dot_file() { + assert_eq!( + build_dot_file(stage_graphs("pipeline{ agent any stages { stage('Build') { steps { sh 'printenv' }} stage('Test') { steps { sh 'cargo test' } } stage('Publish') { steps { sh 'cargo publish' } } }}").unwrap()), + r#"digraph { "Build" -> "Test";"Test" -> "Publish"; }"# + ) + } } diff --git a/src/main.rs b/src/main.rs index 5b51f0a..849b764 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,8 @@ struct JdpOptions { enum Command { #[options(help = "Validate the syntax of a Jenkinsfile")] Check(CheckOpts), + #[options(help = "Print the stages graph of a Jenkinsfile")] + Graph(GraphOpts) } // Options accepted for the `make` command @@ -28,6 +30,15 @@ struct CheckOpts { file: std::path::PathBuf, } +#[derive(Debug, Options)] +struct GraphOpts { + #[options(help = "print help message")] + help: bool, + #[options(free, required, help = "Path to a Jenkinsfile")] + file: std::path::PathBuf, +} + + /// The number of lines of context to show for errors const LINES_OF_CONTEXT: usize = 4; @@ -110,5 +121,10 @@ fn main() { println!("Looks valid! Great work!"); } } + Command::Graph(graphopts) => { + if let Err(_) = parse_graph_stages(&graphopts.file) { + std::process::exit(1); + } + } } } From 77dd3cd79f905cf40dc6befd5a396e29c83f1f32 Mon Sep 17 00:00:00 2001 From: himanoa Date: Sat, 21 Aug 2021 04:01:43 +0900 Subject: [PATCH 2/2] Exec `cargo fmt` --- src/lib.rs | 24 +++++++++++------------- src/main.rs | 3 +-- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f51c23e..4f4b5d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,7 +49,6 @@ pub fn parse_graph_stages(path: &PathBuf) -> Result<(), PestError> { use std::fs::File; use std::io::Read; - match File::open(path) { Ok(mut file) => { let mut contents = String::new(); @@ -94,13 +93,13 @@ fn stage_graphs(buffer: &str) -> Result, PestError> { fn get_stage_names(pairs: Pairs) -> impl Iterator + '_ { pairs.flat_map(|stage| { if let Rule::stage = stage.as_rule() { - if let Some(stage_name_span) = stage.into_inner().next() { + if let Some(stage_name_span) = stage.into_inner().next() { let stage_name = stage_name_span.as_str(); - return Some(&stage_name[1..stage_name.len()-1]); + return Some(&stage_name[1..stage_name.len() - 1]); } return None; } - return None + return None; }) } @@ -112,16 +111,15 @@ fn stage_graphs(buffer: &str) -> Result, PestError> { pest::Position::from_start(buffer), )); } - + let parser = PipelineParser::parse(Rule::pipeline, buffer)?; - if let Some(a) = parser.flat_map(|parsed| { - match parsed.as_rule() { - Rule::stagesDecl => { - Some(get_stage_names(parsed.into_inner())) - } - _ => None - } - }).next() { + if let Some(a) = parser + .flat_map(|parsed| match parsed.as_rule() { + Rule::stagesDecl => Some(get_stage_names(parsed.into_inner())), + _ => None, + }) + .next() + { return Ok(a.collect::>()); } else { return Err(PestError::new_from_pos( diff --git a/src/main.rs b/src/main.rs index 849b764..b54c26f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,7 @@ enum Command { #[options(help = "Validate the syntax of a Jenkinsfile")] Check(CheckOpts), #[options(help = "Print the stages graph of a Jenkinsfile")] - Graph(GraphOpts) + Graph(GraphOpts), } // Options accepted for the `make` command @@ -38,7 +38,6 @@ struct GraphOpts { file: std::path::PathBuf, } - /// The number of lines of context to show for errors const LINES_OF_CONTEXT: usize = 4;