diff --git a/Cargo.lock b/Cargo.lock index 7a27dd9..af5e57d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -223,6 +223,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "either" version = "1.11.0" @@ -466,7 +472,9 @@ dependencies = [ "ratatui", "regex", "rsn", + "schemars", "serde", + "serde_json", "serde_yaml", "stringlit", "toml", @@ -495,6 +503,31 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "schemars" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6e7ed6919cb46507fb01ff1654309219f62b4d603822501b0b80d42f6f21ef" +dependencies = [ + "dyn-clone", + "indexmap", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185f2b7aa7e02d418e453790dde16890256bbd2bcd04b7dc5348811052b53f49" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -521,6 +554,29 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.5" diff --git a/Cargo.toml b/Cargo.toml index 86fd101..02b9606 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,9 @@ indexmap = { version = "2.2.6", features = ["serde"] } ratatui = "0.26.2" regex = "1.10.4" rsn = "0.1.0" +schemars = { version = "0.8.19", features = ["indexmap2"] } serde = { version = "1.0.201", features = ["derive"] } +serde_json = { version = "1.0.117", features = ["indexmap", "preserve_order"] } serde_yaml = "0.9.34" stringlit = "2.1.0" toml = { version = "0.8.12", features = ["indexmap", "preserve_order"] } diff --git a/src/bin/checker.rs b/src/bin/checker.rs deleted file mode 100644 index 5d6f598..0000000 --- a/src/bin/checker.rs +++ /dev/null @@ -1,112 +0,0 @@ -use std::path::PathBuf; - -use clap::Parser; -use indexmap::{ - map::{Keys, Values}, - IndexMap, -}; -use regex::Regex; -use req::*; -use stringlit::s; - -#[derive(Parser)] -struct Args { - #[arg(short, long, default_value = "REQ-.*")] - allowed_requirements: String, - requirements: PathBuf, - test_results: PathBuf, -} - -fn nl() -> String { - s!("") -} - -fn check_requirements( - test_results: &str, - output: &mut Vec, - requirements: &IndexMap, - allowed_requirements: &Regex, -) { - for (id, requirement) in requirements { - if allowed_requirements.is_match(&id) { - let status = if test_results.contains(&format!("{id} succeeded")) { - ":white_check_mark:" - } else if test_results.contains(&format!("{id} failed")) { - ":x:" - } else { - ":warning:" - }; - output.push(format!("- _{id}_ - {}: {status}", requirement.name)); - } - } -} - -fn has_valid_requirements( - mut requirements: Keys, - allowed_requirements: &Regex, -) -> bool { - requirements.any(|id| allowed_requirements.is_match(&id)) -} - -fn has_valid_topics(mut topics: Values, allowed_requirements: &Regex) -> bool { - topics.any(|topic| { - has_valid_requirements(topic.requirements.keys(), allowed_requirements) - || has_valid_topics(topic.subtopics.values(), allowed_requirements) - }) -} - -fn check_topics( - test_results: &str, - output: &mut Vec, - topics: &IndexMap, - allowed_requirements: &Regex, - level: usize, -) { - if !has_valid_topics(topics.values(), allowed_requirements) { - return; - } - for (id, topic) in topics { - if !has_valid_topics(topic.subtopics.values(), allowed_requirements) - && !has_valid_requirements(topic.requirements.keys(), allowed_requirements) - { - continue; - } - output.push(format!("{} _{id}_ - {}", "#".repeat(level), topic.name)); - if !topic.requirements.is_empty() { - check_requirements( - test_results, - output, - &topic.requirements, - allowed_requirements, - ); - output.push(nl()); - } - if !topic.subtopics.is_empty() { - check_topics( - test_results, - output, - &topic.subtopics, - allowed_requirements, - level + 1, - ); - output.push(nl()); - } - } -} - -fn main() -> anyhow::Result<()> { - let Args { - allowed_requirements, - requirements, - test_results, - } = Args::parse(); - let re = Regex::new(&allowed_requirements).unwrap(); - let test_results = std::fs::read_to_string(test_results)?; - let project: Project = serde_yaml::from_str(&std::fs::read_to_string(requirements)?)?; - let mut output = vec![format!("# Test Results - {}", project.name)]; - check_topics(&test_results, &mut output, &project.topics, &re, 2); - - let output = output.join("\n"); - println!("{output}"); - Ok(()) -} diff --git a/src/lib.rs b/src/lib.rs index cfc69b4..29c4225 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,16 @@ use indexmap::{indexmap, IndexMap}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize, Serializer}; use stringlit::s; -pub fn my_trim(v: &String, s: S) -> Result +pub fn my_trim(v: &str, s: S) -> Result where S: Serializer, { s.serialize_str(v.trim()) } -#[derive(Debug, Deserialize, Serialize)] +#[derive(JsonSchema, Debug, Deserialize, Serialize)] pub struct Requirement { pub name: String, #[serde(serialize_with = "my_trim")] @@ -18,7 +19,7 @@ pub struct Requirement { pub requires: Vec, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(JsonSchema, Debug, Deserialize, Serialize)] pub struct Topic { pub name: String, #[serde(default, skip_serializing_if = "IndexMap::is_empty")] @@ -27,7 +28,7 @@ pub struct Topic { pub subtopics: IndexMap, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(JsonSchema, Debug, Deserialize, Serialize)] pub struct Definition { pub name: String, pub value: String, @@ -35,7 +36,7 @@ pub struct Definition { pub additional_info: Vec, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(JsonSchema, Debug, Deserialize, Serialize)] pub struct ConfigDefault { pub name: String, #[serde(rename = "type")] @@ -50,7 +51,7 @@ pub struct ConfigDefault { pub hint: Option, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(JsonSchema, Debug, Deserialize, Serialize)] pub struct Project { pub name: String, #[serde(serialize_with = "my_trim")] @@ -66,10 +67,10 @@ pub struct Project { pub fn demo_project() -> Project { Project { name: s!("journal-uploader"), - description: s!(r#" + description: s!(r" The journal-uploader has two main functionalities. - Take a stream of log messages and filter them depending on their severity -- Upload journal logs for a specified time when activated through cloud call"#), +- Upload journal logs for a specified time when activated through cloud call"), topics: indexmap! { s!("FEAT-1") => Topic { name: s!("Traced Logging"), @@ -79,7 +80,7 @@ The journal-uploader has two main functionalities. requirements: indexmap! { s!("REQ-1") => Requirement { name: s!("Continuous Monitoring"), - description: s!(r#"The tool must continuously monitor a designated directory."#), + description: s!(r"The tool must continuously monitor a designated directory."), requires: vec! [], } }, @@ -90,12 +91,12 @@ The journal-uploader has two main functionalities. requirements: indexmap! { s!("REQ-1") => Requirement { name: s!("Detection of New Files"), - description: s!(r#"The tool must detect the addition of new files in the monitored directory."#), + description: s!(r"The tool must detect the addition of new files in the monitored directory."), requires: vec! [], }, s!("REQ-2") => Requirement { name: s!("Avoid Re-processing"), - description: s!(r#"The tool must not process files that have already been processed."#), + description: s!(r"The tool must not process files that have already been processed."), requires: vec! [], } }, diff --git a/src/main.rs b/src/main.rs index f533ccc..d3c0369 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,13 @@ use std::path::PathBuf; use clap::Parser; -use indexmap::IndexMap; +use indexmap::{ + map::{Keys, Values}, + IndexMap, +}; +use regex::Regex; use req::*; +use schemars::schema_for; use stringlit::s; pub const WORD_DESCRIPTION: &str = // @@ -28,6 +33,79 @@ fn nl() -> String { s!("") } +fn check_requirements( + test_results: &str, + output: &mut Vec, + requirements: &IndexMap, + allowed_requirements: &Regex, +) { + for (id, requirement) in requirements { + if allowed_requirements.is_match(id) { + let status = if test_results.contains(&format!("{id} succeeded")) { + ":white_check_mark:" + } else if test_results.contains(&format!("{id} failed")) { + ":x:" + } else { + ":warning:" + }; + output.push(format!("- _{id}_ - {}: {status}", requirement.name)); + } + } +} + +fn has_valid_requirements( + mut requirements: Keys, + allowed_requirements: &Regex, +) -> bool { + requirements.any(|id| allowed_requirements.is_match(id)) +} + +fn has_valid_topics(mut topics: Values, allowed_requirements: &Regex) -> bool { + topics.any(|topic| { + has_valid_requirements(topic.requirements.keys(), allowed_requirements) + || has_valid_topics(topic.subtopics.values(), allowed_requirements) + }) +} + +fn check_topics( + test_results: &str, + output: &mut Vec, + topics: &IndexMap, + allowed_requirements: &Regex, + level: usize, +) { + if !has_valid_topics(topics.values(), allowed_requirements) { + return; + } + for (id, topic) in topics { + if !has_valid_topics(topic.subtopics.values(), allowed_requirements) + && !has_valid_requirements(topic.requirements.keys(), allowed_requirements) + { + continue; + } + output.push(format!("{} _{id}_ - {}", "#".repeat(level), topic.name)); + if !topic.requirements.is_empty() { + check_requirements( + test_results, + output, + &topic.requirements, + allowed_requirements, + ); + output.push(nl()); + } + if !topic.subtopics.is_empty() { + check_topics( + test_results, + output, + &topic.subtopics, + allowed_requirements, + level + 1, + ); + output.push(nl()); + } + } +} + fn add_requirements(output: &mut Vec, requirements: &IndexMap) { for (id, requirement) in requirements { output.push(format!( @@ -51,77 +129,115 @@ fn add_topics(output: &mut Vec, topics: &IndexMap, level: } } +#[derive(Parser)] +enum Command { + Schema, + #[clap(alias = "md")] + Markdown { + requirements: PathBuf, + }, + Check { + #[arg(short, long, default_value = "REQ-.*")] + allowed_requirements: String, + requirements: PathBuf, + test_results: PathBuf, + }, +} + #[derive(Parser)] struct Args { - input: PathBuf, + #[clap(subcommand)] + command: Command, } fn main() -> anyhow::Result<()> { - let Args { input } = Args::parse(); - let project: Project = serde_yaml::from_str(&std::fs::read_to_string(input)?)?; - - let mut output = vec![ - format!("# Requirements for {}", project.name), - nl(), - s!("[[_TOC_]]"), - nl(), - WORD_DESCRIPTION.to_string(), - nl(), - s!("## Description"), - project.description, - nl(), - ]; - - if !project.topics.is_empty() { - output.push(s!("## Requirements")); - add_topics(&mut output, &project.topics, 3); - output.push(nl()); - } - - if !project.definitions.is_empty() { - output.push(s!("## Definitions")); - for definition in project.definitions { - output.push(format!("- {}: {}", definition.name, definition.value)); - for info in definition.additional_info { - output.push(format!(" - {info}")) - } + let Args { command } = Args::parse(); + match command { + Command::Schema => { + let schema = schema_for!(Project); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); } - output.push(nl()); - } + Command::Markdown { requirements } => { + let project: Project = serde_yaml::from_str(&std::fs::read_to_string(requirements)?)?; - if !project.config_defaults.is_empty() { - output.push(s!("## Config Defaults")); - for default in project.config_defaults { - output.push(format!("- **{}**", default.name)); - output.push(format!(" - Type: {}", default.typ)); - if let Some(unit) = default.unit { - output.push(format!(" - Unit: {unit}")); + let mut output = vec![ + format!("# Requirements for {}", project.name), + nl(), + s!("[[_TOC_]]"), + nl(), + WORD_DESCRIPTION.to_string(), + nl(), + s!("## Description"), + project.description, + nl(), + ]; + + if !project.topics.is_empty() { + output.push(s!("## Requirements")); + add_topics(&mut output, &project.topics, 3); + output.push(nl()); } - if let Some(valid_values) = default.valid_values { - output.push(format!(" - Valid Values: _{}_", valid_values.join(", "))); + + if !project.definitions.is_empty() { + output.push(s!("## Definitions")); + for definition in project.definitions { + output.push(format!("- {}: {}", definition.name, definition.value)); + for info in definition.additional_info { + output.push(format!(" - {info}")) + } + } + output.push(nl()); } - if let Some(default_value) = default.default_value { - output.push(format!( - " - Default Value: _{}_{}", - default_value, - default.hint.map(|h| format!(" {h}")).unwrap_or_default() - )); - } else { - output.push(format!( + + if !project.config_defaults.is_empty() { + output.push(s!("## Config Defaults")); + for default in project.config_defaults { + output.push(format!("- **{}**", default.name)); + output.push(format!(" - Type: {}", default.typ)); + if let Some(unit) = default.unit { + output.push(format!(" - Unit: {unit}")); + } + if let Some(valid_values) = default.valid_values { + output.push(format!(" - Valid Values: _{}_", valid_values.join(", "))); + } + if let Some(default_value) = default.default_value { + output.push(format!( + " - Default Value: _{}_{}", + default_value, + default.hint.map(|h| format!(" {h}")).unwrap_or_default() + )); + } else { + output.push(format!( " - **Required**: This value **_MUST_** be provided as a start parameter.{}", default.hint.map(|h| format!(" {h}")).unwrap_or_default() )); + } + output.push(nl()); + } } - output.push(nl()); + + let mut output = output.join("\n"); + for word in HIGHLIGHTED_WORDS { + output = output.replace(word, &format!("**_{}_**", word.to_uppercase())); + } + + println!("{output}"); + } + Command::Check { + allowed_requirements, + requirements, + test_results, + } => { + let re = Regex::new(&allowed_requirements).unwrap(); + let test_results = std::fs::read_to_string(test_results)?; + let project: Project = serde_yaml::from_str(&std::fs::read_to_string(requirements)?)?; + let mut output = vec![format!("# Test Results - {}", project.name)]; + check_topics(&test_results, &mut output, &project.topics, &re, 2); + + let output = output.join("\n"); + println!("{output}"); } } - let mut output = output.join("\n"); - for word in HIGHLIGHTED_WORDS { - output = output.replace(word, &format!("**_{}_**", word.to_uppercase())); - } - - println!("{output}"); - Ok(()) }