This commit is contained in:
Steve Biedermann 2024-05-09 22:17:00 +02:00
parent 5b2fdd380a
commit d9e9ce628c
5 changed files with 243 additions and 180 deletions

56
Cargo.lock generated
View File

@ -223,6 +223,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "dyn-clone"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
[[package]] [[package]]
name = "either" name = "either"
version = "1.11.0" version = "1.11.0"
@ -466,7 +472,9 @@ dependencies = [
"ratatui", "ratatui",
"regex", "regex",
"rsn", "rsn",
"schemars",
"serde", "serde",
"serde_json",
"serde_yaml", "serde_yaml",
"stringlit", "stringlit",
"toml", "toml",
@ -495,6 +503,31 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 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]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@ -521,6 +554,29 @@ dependencies = [
"syn", "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]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.5" version = "0.6.5"

View File

@ -14,7 +14,9 @@ indexmap = { version = "2.2.6", features = ["serde"] }
ratatui = "0.26.2" ratatui = "0.26.2"
regex = "1.10.4" regex = "1.10.4"
rsn = "0.1.0" rsn = "0.1.0"
schemars = { version = "0.8.19", features = ["indexmap2"] }
serde = { version = "1.0.201", features = ["derive"] } serde = { version = "1.0.201", features = ["derive"] }
serde_json = { version = "1.0.117", features = ["indexmap", "preserve_order"] }
serde_yaml = "0.9.34" serde_yaml = "0.9.34"
stringlit = "2.1.0" stringlit = "2.1.0"
toml = { version = "0.8.12", features = ["indexmap", "preserve_order"] } toml = { version = "0.8.12", features = ["indexmap", "preserve_order"] }

View File

@ -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<String>,
requirements: &IndexMap<String, Requirement>,
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<String, Requirement>,
allowed_requirements: &Regex,
) -> bool {
requirements.any(|id| allowed_requirements.is_match(&id))
}
fn has_valid_topics(mut topics: Values<String, Topic>, 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<String>,
topics: &IndexMap<String, Topic>,
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(())
}

View File

@ -1,15 +1,16 @@
use indexmap::{indexmap, IndexMap}; use indexmap::{indexmap, IndexMap};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, Serializer}; use serde::{Deserialize, Serialize, Serializer};
use stringlit::s; use stringlit::s;
pub fn my_trim<S>(v: &String, s: S) -> Result<S::Ok, S::Error> pub fn my_trim<S>(v: &str, s: S) -> Result<S::Ok, S::Error>
where where
S: Serializer, S: Serializer,
{ {
s.serialize_str(v.trim()) s.serialize_str(v.trim())
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(JsonSchema, Debug, Deserialize, Serialize)]
pub struct Requirement { pub struct Requirement {
pub name: String, pub name: String,
#[serde(serialize_with = "my_trim")] #[serde(serialize_with = "my_trim")]
@ -18,7 +19,7 @@ pub struct Requirement {
pub requires: Vec<String>, pub requires: Vec<String>,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(JsonSchema, Debug, Deserialize, Serialize)]
pub struct Topic { pub struct Topic {
pub name: String, pub name: String,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")] #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
@ -27,7 +28,7 @@ pub struct Topic {
pub subtopics: IndexMap<String, Topic>, pub subtopics: IndexMap<String, Topic>,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(JsonSchema, Debug, Deserialize, Serialize)]
pub struct Definition { pub struct Definition {
pub name: String, pub name: String,
pub value: String, pub value: String,
@ -35,7 +36,7 @@ pub struct Definition {
pub additional_info: Vec<String>, pub additional_info: Vec<String>,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(JsonSchema, Debug, Deserialize, Serialize)]
pub struct ConfigDefault { pub struct ConfigDefault {
pub name: String, pub name: String,
#[serde(rename = "type")] #[serde(rename = "type")]
@ -50,7 +51,7 @@ pub struct ConfigDefault {
pub hint: Option<String>, pub hint: Option<String>,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(JsonSchema, Debug, Deserialize, Serialize)]
pub struct Project { pub struct Project {
pub name: String, pub name: String,
#[serde(serialize_with = "my_trim")] #[serde(serialize_with = "my_trim")]
@ -66,10 +67,10 @@ pub struct Project {
pub fn demo_project() -> Project { pub fn demo_project() -> Project {
Project { Project {
name: s!("journal-uploader"), name: s!("journal-uploader"),
description: s!(r#" description: s!(r"
The journal-uploader has two main functionalities. The journal-uploader has two main functionalities.
- Take a stream of log messages and filter them depending on their severity - 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! { topics: indexmap! {
s!("FEAT-1") => Topic { s!("FEAT-1") => Topic {
name: s!("Traced Logging"), name: s!("Traced Logging"),
@ -79,7 +80,7 @@ The journal-uploader has two main functionalities.
requirements: indexmap! { requirements: indexmap! {
s!("REQ-1") => Requirement { s!("REQ-1") => Requirement {
name: s!("Continuous Monitoring"), 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! [], requires: vec! [],
} }
}, },
@ -90,12 +91,12 @@ The journal-uploader has two main functionalities.
requirements: indexmap! { requirements: indexmap! {
s!("REQ-1") => Requirement { s!("REQ-1") => Requirement {
name: s!("Detection of New Files"), 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! [], requires: vec! [],
}, },
s!("REQ-2") => Requirement { s!("REQ-2") => Requirement {
name: s!("Avoid Re-processing"), 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! [], requires: vec! [],
} }
}, },

View File

@ -1,8 +1,13 @@
use std::path::PathBuf; use std::path::PathBuf;
use clap::Parser; use clap::Parser;
use indexmap::IndexMap; use indexmap::{
map::{Keys, Values},
IndexMap,
};
use regex::Regex;
use req::*; use req::*;
use schemars::schema_for;
use stringlit::s; use stringlit::s;
pub const WORD_DESCRIPTION: &str = // pub const WORD_DESCRIPTION: &str = //
@ -28,6 +33,79 @@ fn nl() -> String {
s!("") s!("")
} }
fn check_requirements(
test_results: &str,
output: &mut Vec<String>,
requirements: &IndexMap<String, Requirement>,
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<String, Requirement>,
allowed_requirements: &Regex,
) -> bool {
requirements.any(|id| allowed_requirements.is_match(id))
}
fn has_valid_topics(mut topics: Values<String, Topic>, 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<String>,
topics: &IndexMap<String, Topic>,
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<String>, requirements: &IndexMap<String, Requirement>) { fn add_requirements(output: &mut Vec<String>, requirements: &IndexMap<String, Requirement>) {
for (id, requirement) in requirements { for (id, requirement) in requirements {
output.push(format!( output.push(format!(
@ -51,14 +129,36 @@ fn add_topics(output: &mut Vec<String>, topics: &IndexMap<String, Topic>, 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)] #[derive(Parser)]
struct Args { struct Args {
input: PathBuf, #[clap(subcommand)]
command: Command,
} }
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
let Args { input } = Args::parse(); let Args { command } = Args::parse();
let project: Project = serde_yaml::from_str(&std::fs::read_to_string(input)?)?; match command {
Command::Schema => {
let schema = schema_for!(Project);
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
}
Command::Markdown { requirements } => {
let project: Project = serde_yaml::from_str(&std::fs::read_to_string(requirements)?)?;
let mut output = vec![ let mut output = vec![
format!("# Requirements for {}", project.name), format!("# Requirements for {}", project.name),
@ -122,6 +222,22 @@ fn main() -> anyhow::Result<()> {
} }
println!("{output}"); 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}");
}
}
Ok(()) Ok(())
} }