2024-05-08 13:50:35 +00:00
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
2024-05-12 21:27:33 +00:00
|
|
|
use clap::{CommandFactory, Parser, Subcommand};
|
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,
|
2024-05-10 11:30:27 +00:00
|
|
|
output: &mut IndexMap<String, bool>,
|
2024-05-09 20:17:00 +00:00
|
|
|
requirements: &IndexMap<String, Requirement>,
|
|
|
|
|
allowed_requirements: &Regex,
|
|
|
|
|
) {
|
2024-05-12 21:27:33 +00:00
|
|
|
for (id, _) in requirements {
|
2024-05-09 20:17:00 +00:00
|
|
|
if allowed_requirements.is_match(id) {
|
2024-05-10 11:30:27 +00:00
|
|
|
if test_results.contains(&format!("{}: failed", id.trim())) {
|
|
|
|
|
output.insert(id.trim().to_string(), false);
|
|
|
|
|
} else if test_results.contains(&format!("{}: passed", id.trim())) {
|
|
|
|
|
output.entry(id.trim().to_string()).or_insert(true);
|
2024-05-09 20:17:00 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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(
|
2024-05-10 11:30:27 +00:00
|
|
|
test_results: &[PathBuf],
|
2024-05-09 20:17:00 +00:00
|
|
|
output: &mut Vec<String>,
|
|
|
|
|
topics: &IndexMap<String, Topic>,
|
|
|
|
|
allowed_requirements: &Regex,
|
|
|
|
|
level: usize,
|
2024-05-10 11:30:27 +00:00
|
|
|
) -> anyhow::Result<()> {
|
2024-05-09 20:17:00 +00:00
|
|
|
if !has_valid_topics(topics.values(), allowed_requirements) {
|
2024-05-10 11:30:27 +00:00
|
|
|
return Ok(());
|
2024-05-09 20:17:00 +00:00
|
|
|
}
|
|
|
|
|
for (id, topic) in topics {
|
|
|
|
|
if !has_valid_topics(topic.subtopics.values(), allowed_requirements)
|
|
|
|
|
&& !has_valid_requirements(topic.requirements.keys(), allowed_requirements)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2024-05-10 08:23:49 +00:00
|
|
|
output.push(format!(
|
|
|
|
|
"{} _{}_ - {}",
|
|
|
|
|
"#".repeat(level),
|
|
|
|
|
id.trim(),
|
|
|
|
|
topic.name
|
|
|
|
|
));
|
2024-05-10 11:30:27 +00:00
|
|
|
|
|
|
|
|
let mut test_status = IndexMap::new();
|
|
|
|
|
for test_result in test_results {
|
|
|
|
|
let test_result = std::fs::read_to_string(test_result)?;
|
|
|
|
|
if !topic.requirements.is_empty() {
|
|
|
|
|
check_requirements(
|
|
|
|
|
&test_result,
|
|
|
|
|
&mut test_status,
|
|
|
|
|
&topic.requirements,
|
|
|
|
|
allowed_requirements,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-09 20:17:00 +00:00
|
|
|
if !topic.requirements.is_empty() {
|
2024-05-10 11:30:27 +00:00
|
|
|
for (id, req) in &topic.requirements {
|
|
|
|
|
let status = if let Some(status) = test_status.get(id) {
|
|
|
|
|
if *status {
|
|
|
|
|
":white_check_mark:"
|
|
|
|
|
} else {
|
|
|
|
|
":x:"
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
":warning:"
|
|
|
|
|
};
|
|
|
|
|
output.push(format!("- _{}_ - {}: {status}", id.trim(), req.name));
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-09 20:17:00 +00:00
|
|
|
output.push(nl());
|
|
|
|
|
}
|
2024-05-10 11:30:27 +00:00
|
|
|
|
2024-05-09 20:17:00 +00:00
|
|
|
if !topic.subtopics.is_empty() {
|
|
|
|
|
check_topics(
|
|
|
|
|
test_results,
|
|
|
|
|
output,
|
|
|
|
|
&topic.subtopics,
|
|
|
|
|
allowed_requirements,
|
|
|
|
|
level + 1,
|
2024-05-10 11:30:27 +00:00
|
|
|
)?;
|
2024-05-09 20:17:00 +00:00
|
|
|
output.push(nl());
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-05-10 11:30:27 +00:00
|
|
|
Ok(())
|
2024-05-09 20:17:00 +00:00
|
|
|
}
|
|
|
|
|
|
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-10 08:23:49 +00:00
|
|
|
"- **_{}_ - {}:** {}",
|
|
|
|
|
id.trim(),
|
2024-05-09 21:14:14 +00:00
|
|
|
requirement.name.trim(),
|
|
|
|
|
requirement.description.trim()
|
2024-05-08 12:18:03 +00:00
|
|
|
));
|
2024-05-10 08:23:49 +00:00
|
|
|
for info in &requirement.additional_info {
|
|
|
|
|
output.push(format!(" - {}", info.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!(
|
2024-05-10 08:23:49 +00:00
|
|
|
"{} _{}_ - {}",
|
2024-05-09 21:14:14 +00:00
|
|
|
"#".repeat(level),
|
2024-05-10 08:23:49 +00:00
|
|
|
id.trim(),
|
2024-05-09 21:14:14 +00:00
|
|
|
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-12 21:27:33 +00:00
|
|
|
#[derive(Subcommand)]
|
2024-05-09 20:17:00 +00:00
|
|
|
enum Command {
|
2024-05-10 09:39:01 +00:00
|
|
|
/// Outputs the JSON schema for the input data
|
2024-05-09 20:17:00 +00:00
|
|
|
Schema,
|
2024-05-10 09:39:01 +00:00
|
|
|
/// Outputs demo data in YAML format
|
2024-05-09 21:14:14 +00:00
|
|
|
Demo,
|
2024-05-09 20:17:00 +00:00
|
|
|
#[clap(alias = "md")]
|
2024-05-10 09:39:01 +00:00
|
|
|
/// Transform requirements into Markdown
|
2024-05-09 20:17:00 +00:00
|
|
|
Markdown {
|
2024-05-10 09:39:01 +00:00
|
|
|
/// The path to the requirements file
|
2024-05-09 20:17:00 +00:00
|
|
|
requirements: PathBuf,
|
|
|
|
|
},
|
2024-05-10 09:39:01 +00:00
|
|
|
/// Transform requirements into HTML
|
2024-05-10 08:23:49 +00:00
|
|
|
Html {
|
2024-05-10 09:39:01 +00:00
|
|
|
/// The path to the requirements file
|
2024-05-10 08:23:49 +00:00
|
|
|
requirements: PathBuf,
|
|
|
|
|
},
|
2024-05-10 09:39:01 +00:00
|
|
|
/// Check test output against requirements
|
2024-05-09 20:17:00 +00:00
|
|
|
Check {
|
|
|
|
|
#[arg(short, long, default_value = "REQ-.*")]
|
2024-05-10 09:39:01 +00:00
|
|
|
/// Regex to select which requirements should be checked
|
2024-05-09 20:17:00 +00:00
|
|
|
allowed_requirements: String,
|
2024-05-10 09:39:01 +00:00
|
|
|
/// The path to the requirements file
|
2024-05-09 20:17:00 +00:00
|
|
|
requirements: PathBuf,
|
2024-05-10 11:30:27 +00:00
|
|
|
/// The path to the test output files
|
|
|
|
|
#[arg(required=true, num_args=1..)]
|
|
|
|
|
test_results: Vec<PathBuf>,
|
2024-05-09 20:17:00 +00:00
|
|
|
},
|
2024-05-12 21:27:33 +00:00
|
|
|
/// Generate shell completions
|
|
|
|
|
Completions {
|
|
|
|
|
/// The shell to generate the completions for
|
|
|
|
|
#[arg(value_enum)]
|
|
|
|
|
shell: clap_complete_command::Shell,
|
|
|
|
|
},
|
2024-05-09 20:17:00 +00:00
|
|
|
}
|
|
|
|
|
|
2024-05-08 13:50:35 +00:00
|
|
|
#[derive(Parser)]
|
2024-05-10 11:30:27 +00:00
|
|
|
#[command(version)]
|
2024-05-08 13:50:35 +00:00
|
|
|
struct Args {
|
2024-05-09 20:17:00 +00:00
|
|
|
#[clap(subcommand)]
|
|
|
|
|
command: Command,
|
2024-05-08 13:50:35 +00:00
|
|
|
}
|
|
|
|
|
|
2024-05-10 08:23:49 +00:00
|
|
|
fn parse(value: &str) -> anyhow::Result<Project> {
|
|
|
|
|
Ok(serde_yaml::from_str(value)
|
|
|
|
|
.or_else(|_| serde_json::from_str(value))
|
|
|
|
|
.or_else(|_| rsn::from_str(value))
|
|
|
|
|
.or_else(|_| toml::from_str(value))?)
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-10 09:39:01 +00:00
|
|
|
fn to_markdown(requirements: PathBuf, add_toc: bool) -> anyhow::Result<String> {
|
2024-05-10 08:23:49 +00:00
|
|
|
let project: Project = parse(&std::fs::read_to_string(requirements)?)?;
|
|
|
|
|
|
2024-05-10 09:39:01 +00:00
|
|
|
let mut output = vec![format!("# Requirements for {}", project.name.trim()), nl()];
|
|
|
|
|
if add_toc {
|
|
|
|
|
output.extend([s!("[[_TOC_]]"), nl()]);
|
|
|
|
|
}
|
|
|
|
|
output.extend([
|
2024-05-10 08:23:49 +00:00
|
|
|
WORD_DESCRIPTION.trim().to_string(),
|
|
|
|
|
nl(),
|
|
|
|
|
format!("**VERSION: {}**", project.version),
|
|
|
|
|
nl(),
|
|
|
|
|
s!("## Description"),
|
|
|
|
|
project.description.trim().to_string(),
|
|
|
|
|
nl(),
|
2024-05-10 09:39:01 +00:00
|
|
|
]);
|
2024-05-10 08:23:49 +00:00
|
|
|
|
|
|
|
|
if !project.topics.is_empty() {
|
|
|
|
|
output.push(s!("## Requirements"));
|
|
|
|
|
add_topics(&mut output, &project.topics, 3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !project.definitions.is_empty() {
|
|
|
|
|
output.push(s!("## Definitions"));
|
|
|
|
|
for definition in project.definitions {
|
|
|
|
|
output.push(format!(
|
|
|
|
|
"- {}: {}",
|
|
|
|
|
definition.name.trim(),
|
|
|
|
|
definition.value.trim()
|
|
|
|
|
));
|
|
|
|
|
for info in definition.additional_info {
|
|
|
|
|
output.push(format!(" - {}", info.trim()))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
output.push(nl());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !project.config_defaults.is_empty() {
|
|
|
|
|
output.push(s!("## Config Defaults"));
|
|
|
|
|
for default in project.config_defaults {
|
|
|
|
|
output.push(format!("- **{}**", default.name.trim()));
|
|
|
|
|
output.push(format!(" - Type: {}", default.typ.trim()));
|
|
|
|
|
if let Some(unit) = default.unit {
|
|
|
|
|
output.push(format!(" - Unit: {}", unit.trim()));
|
|
|
|
|
}
|
|
|
|
|
if let Some(valid_values) = default.valid_values {
|
|
|
|
|
output.push(format!(
|
|
|
|
|
" - Valid Values: _{}_",
|
|
|
|
|
valid_values.join(", ").trim()
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if let Some(default_value) = default.default_value {
|
|
|
|
|
output.push(format!(
|
|
|
|
|
" - Default Value: _{}_{}",
|
|
|
|
|
default_value.trim(),
|
|
|
|
|
default
|
|
|
|
|
.hint
|
|
|
|
|
.map(|h| format!(" {}", h.trim()))
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
));
|
|
|
|
|
} else {
|
|
|
|
|
output.push(format!(
|
|
|
|
|
" - **Required**: This value **_MUST_** be provided as a start parameter.{}",
|
|
|
|
|
default
|
|
|
|
|
.hint
|
|
|
|
|
.map(|h| format!(" {}", h.trim()))
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
output.push(nl());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut output = output.join("\n");
|
|
|
|
|
for word in HIGHLIGHTED_WORDS {
|
|
|
|
|
output = output.replace(word, &format!("**_{}_**", word.to_uppercase()));
|
|
|
|
|
}
|
|
|
|
|
Ok(output)
|
|
|
|
|
}
|
|
|
|
|
|
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-10 08:23:49 +00:00
|
|
|
Command::Html { requirements } => {
|
2024-05-10 09:39:01 +00:00
|
|
|
let output = to_markdown(requirements, false)?;
|
2024-05-12 21:27:33 +00:00
|
|
|
let template = include_str!("../template.html");
|
2024-05-10 08:23:49 +00:00
|
|
|
println!(
|
|
|
|
|
"{}",
|
2024-05-12 21:27:33 +00:00
|
|
|
template.replace(
|
|
|
|
|
"{{content}}",
|
|
|
|
|
&markdown::to_html_with_options(&output, &markdown::Options::gfm())
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("{e}"))?
|
|
|
|
|
)
|
2024-05-10 08:23:49 +00:00
|
|
|
);
|
|
|
|
|
}
|
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 } => {
|
2024-05-10 09:39:01 +00:00
|
|
|
let output = to_markdown(requirements, true)?;
|
2024-05-09 20:17:00 +00:00
|
|
|
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();
|
2024-05-10 08:23:49 +00:00
|
|
|
let project: Project = parse(&std::fs::read_to_string(requirements)?)?;
|
2024-05-09 20:17:00 +00:00
|
|
|
let mut output = vec![format!("# Test Results - {}", project.name)];
|
2024-05-10 11:30:27 +00:00
|
|
|
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-12 21:27:33 +00:00
|
|
|
Command::Completions { shell } => {
|
|
|
|
|
shell.generate(&mut Args::command(), &mut std::io::stdout());
|
|
|
|
|
}
|
2024-05-08 12:43:29 +00:00
|
|
|
}
|
|
|
|
|
|
2024-05-08 09:52:34 +00:00
|
|
|
Ok(())
|
|
|
|
|
}
|
2024-05-12 21:27:33 +00:00
|
|
|
|