req/src/main.rs

263 lines
8.1 KiB
Rust
Raw Normal View History

2024-05-08 13:50:35 +00:00
use std::path::PathBuf;
use clap::Parser;
2024-05-09 20:17:00 +00:00
use indexmap::{
map::{Keys, Values},
IndexMap,
};
use regex::Regex;
2024-05-08 09:52:34 +00:00
use req::*;
2024-05-09 20:17:00 +00:00
use schemars::schema_for;
2024-05-08 12:18:03 +00:00
use stringlit::s;
2024-05-08 12:43:29 +00:00
pub const WORD_DESCRIPTION: &str = //
r#"The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED",
"MAY", and "OPTIONAL" in this document are to be interpreted as described in
[RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119).
"#;
pub const HIGHLIGHTED_WORDS: [&str; 10] = [
"must not",
"must",
"required",
"shall not",
"shall",
"should not",
"should",
"recommended",
"may",
"optional",
];
2024-05-08 12:18:03 +00:00
fn nl() -> String {
s!("")
}
2024-05-09 20:17:00 +00:00
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());
}
}
}
2024-05-08 12:18:03 +00:00
fn add_requirements(output: &mut Vec<String>, requirements: &IndexMap<String, Requirement>) {
for (id, requirement) in requirements {
output.push(format!(
2024-05-08 12:44:43 +00:00
"- **_{id}_ - {}:** {}",
2024-05-09 21:14:14 +00:00
requirement.name.trim(),
requirement.description.trim()
2024-05-08 12:18:03 +00:00
));
}
}
fn add_topics(output: &mut Vec<String>, topics: &IndexMap<String, Topic>, level: usize) {
for (id, topic) in topics {
2024-05-09 21:14:14 +00:00
output.push(format!(
"{} _{id}_ - {}",
"#".repeat(level),
topic.name.trim()
));
2024-05-08 12:18:03 +00:00
if !topic.requirements.is_empty() {
add_requirements(output, &topic.requirements);
output.push(nl());
}
if !topic.subtopics.is_empty() {
add_topics(output, &topic.subtopics, level + 1);
}
}
}
2024-05-08 09:52:34 +00:00
2024-05-09 20:17:00 +00:00
#[derive(Parser)]
enum Command {
Schema,
2024-05-09 21:14:14 +00:00
Demo,
2024-05-09 20:17:00 +00:00
#[clap(alias = "md")]
Markdown {
requirements: PathBuf,
},
Check {
#[arg(short, long, default_value = "REQ-.*")]
allowed_requirements: String,
requirements: PathBuf,
test_results: PathBuf,
},
}
2024-05-08 13:50:35 +00:00
#[derive(Parser)]
struct Args {
2024-05-09 20:17:00 +00:00
#[clap(subcommand)]
command: Command,
2024-05-08 13:50:35 +00:00
}
2024-05-08 09:52:34 +00:00
fn main() -> anyhow::Result<()> {
2024-05-09 20:17:00 +00:00
let Args { command } = Args::parse();
match command {
2024-05-09 21:14:14 +00:00
Command::Demo => {
println!("{}", serde_yaml::to_string(&demo_project())?);
}
2024-05-09 20:17:00 +00:00
Command::Schema => {
let schema = schema_for!(Project);
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
2024-05-08 12:18:03 +00:00
}
2024-05-09 20:17:00 +00:00
Command::Markdown { requirements } => {
let project: Project = serde_yaml::from_str(&std::fs::read_to_string(requirements)?)?;
let mut output = vec![
2024-05-09 21:14:14 +00:00
format!("# Requirements for {}", project.name.trim()),
2024-05-09 20:17:00 +00:00
nl(),
s!("[[_TOC_]]"),
nl(),
2024-05-09 21:14:14 +00:00
WORD_DESCRIPTION.trim().to_string(),
nl(),
format!("**VERSION: {}**", project.version),
2024-05-09 20:17:00 +00:00
nl(),
s!("## Description"),
2024-05-09 21:14:14 +00:00
project.description.trim().to_string(),
2024-05-09 20:17:00 +00:00
nl(),
];
2024-05-08 12:18:03 +00:00
2024-05-09 20:17:00 +00:00
if !project.topics.is_empty() {
output.push(s!("## Requirements"));
add_topics(&mut output, &project.topics, 3);
2024-05-08 12:18:03 +00:00
}
2024-05-09 20:17:00 +00:00
if !project.definitions.is_empty() {
output.push(s!("## Definitions"));
for definition in project.definitions {
2024-05-09 21:14:14 +00:00
output.push(format!(
"- {}: {}",
definition.name.trim(),
definition.value.trim()
));
2024-05-09 20:17:00 +00:00
for info in definition.additional_info {
2024-05-09 21:14:14 +00:00
output.push(format!(" - {}", info.trim()))
2024-05-09 20:17:00 +00:00
}
}
output.push(nl());
2024-05-08 12:18:03 +00:00
}
2024-05-09 20:17:00 +00:00
if !project.config_defaults.is_empty() {
output.push(s!("## Config Defaults"));
for default in project.config_defaults {
2024-05-09 21:14:14 +00:00
output.push(format!("- **{}**", default.name.trim()));
output.push(format!(" - Type: {}", default.typ.trim()));
2024-05-09 20:17:00 +00:00
if let Some(unit) = default.unit {
2024-05-09 21:14:14 +00:00
output.push(format!(" - Unit: {}", unit.trim()));
2024-05-09 20:17:00 +00:00
}
if let Some(valid_values) = default.valid_values {
2024-05-09 21:14:14 +00:00
output.push(format!(
" - Valid Values: _{}_",
valid_values.join(", ").trim()
));
2024-05-09 20:17:00 +00:00
}
if let Some(default_value) = default.default_value {
output.push(format!(
" - Default Value: _{}_{}",
2024-05-09 21:14:14 +00:00
default_value.trim(),
default
.hint
.map(|h| format!(" {}", h.trim()))
.unwrap_or_default()
2024-05-09 20:17:00 +00:00
));
} else {
output.push(format!(
2024-05-08 12:18:03 +00:00
" - **Required**: This value **_MUST_** be provided as a start parameter.{}",
2024-05-09 21:14:14 +00:00
default.hint.map(|h| format!(" {}", h.trim())).unwrap_or_default()
2024-05-08 12:18:03 +00:00
));
2024-05-09 20:17:00 +00:00
}
output.push(nl());
}
2024-05-08 12:18:03 +00:00
}
2024-05-09 20:17:00 +00:00
let mut output = output.join("\n");
for word in HIGHLIGHTED_WORDS {
output = output.replace(word, &format!("**_{}_**", word.to_uppercase()));
}
println!("{output}");
2024-05-08 12:18:03 +00:00
}
2024-05-09 20:17:00 +00:00
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);
2024-05-08 12:18:03 +00:00
2024-05-09 20:17:00 +00:00
let output = output.join("\n");
println!("{output}");
}
2024-05-08 12:43:29 +00:00
}
2024-05-08 09:52:34 +00:00
Ok(())
}