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",
]
[[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"

View File

@ -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"] }

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 schemars::JsonSchema;
use serde::{Deserialize, Serialize, Serializer};
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
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<String>,
}
#[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<String, Topic>,
}
#[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<String>,
}
#[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<String>,
}
#[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! [],
}
},

View File

@ -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<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>) {
for (id, requirement) in requirements {
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)]
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 Args { command } = Args::parse();
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![
format!("# Requirements for {}", project.name),
@ -122,6 +222,22 @@ fn main() -> anyhow::Result<()> {
}
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(())
}