update
This commit is contained in:
parent
26ea87b295
commit
eb246b7b29
|
|
@ -145,6 +145,47 @@ dependencies = [
|
|||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd79504325bf38b10165b02e89b4347300f855f273c4cb30c4a3209e6583275e"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete_command"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "183495371ea78d4c9ff638bfc6497d46fed2396e4f9c50aebc1278a4a9919a3d"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"clap_complete_fig",
|
||||
"clap_complete_nushell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete_fig"
|
||||
version = "4.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54b3e65f91fabdd23cac3d57d39d5d938b4daabd070c335c006dccb866a61110"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_complete",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete_nushell"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d02bc8b1a18ee47c4d2eec3fb5ac034dc68ebea6125b1509e9ccdffcddce66e"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_complete",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.4"
|
||||
|
|
@ -476,6 +517,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"clap_complete_command",
|
||||
"crossterm 0.27.0",
|
||||
"indexmap",
|
||||
"markdown",
|
||||
|
|
|
|||
13
Cargo.toml
13
Cargo.toml
|
|
@ -1,18 +1,21 @@
|
|||
[package]
|
||||
name = "req"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
default-run = "req"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[profile.release]
|
||||
strip = "symbols"
|
||||
lto = "fat"
|
||||
split-debuginfo = "true"
|
||||
codegen-units = 1
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.83"
|
||||
clap = { version = "4.5.4", features = ["derive"] }
|
||||
crossterm = "0.27.0"
|
||||
clap_complete_command = "0.5.1"
|
||||
indexmap = { version = "2.2.6", features = ["serde"] }
|
||||
markdown = "1.0.0-alpha.17"
|
||||
ratatui = "0.26.2"
|
||||
regex = "1.10.4"
|
||||
rsn = "0.1.0"
|
||||
schemars = { version = "0.8.19", features = ["indexmap2"] }
|
||||
|
|
@ -21,5 +24,3 @@ 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"] }
|
||||
tui = "0.19.0"
|
||||
version_operators = "0.0.1"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,6 @@ mkdir -p out
|
|||
cargo build
|
||||
cargo run -q -- schema > out/schema.json
|
||||
cargo run -q -- demo > out/demo.yml
|
||||
cargo run -q -- md req.yml > out/requirements.md
|
||||
cargo run -q -- html req.yml > out/requirements.html
|
||||
cargo run -q -- check req.yml test_result.txt > out/text_result.md
|
||||
cargo run -q -- md requirements.yml > out/requirements.md
|
||||
cargo run -q -- html requirements.yml > out/requirements.html
|
||||
cargo run -q -- check requirements.yml test_result.txt > out/test_result.md
|
||||
|
|
|
|||
177
src/bin/tui.rs
177
src/bin/tui.rs
|
|
@ -1,177 +0,0 @@
|
|||
use crossterm::{
|
||||
event::{self, Event as CEvent, KeyCode},
|
||||
execute,
|
||||
style::Color,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
layout::{Constraint, Direction, Layout},
|
||||
prelude::Style,
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use req::*;
|
||||
|
||||
enum Event<I> {
|
||||
Input(I),
|
||||
Tick,
|
||||
}
|
||||
|
||||
struct App {
|
||||
project: Project,
|
||||
topics_list_state: ListState,
|
||||
requirements_list_state: ListState,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new(project: Project) -> App {
|
||||
let mut topics_list_state = ListState::default();
|
||||
topics_list_state.select(Some(0));
|
||||
|
||||
let mut requirements_list_state = ListState::default();
|
||||
requirements_list_state.select(Some(0));
|
||||
|
||||
App {
|
||||
project,
|
||||
topics_list_state,
|
||||
requirements_list_state,
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self, f: &mut ratatui::Frame) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(10), // Project Title
|
||||
Constraint::Percentage(45), // Topics List
|
||||
Constraint::Percentage(45), // Requirements List
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
let project_title = Block::default()
|
||||
.title(self.project.name.clone())
|
||||
.borders(Borders::ALL);
|
||||
f.render_widget(project_title, chunks[0]);
|
||||
|
||||
let topics: Vec<ListItem> = self
|
||||
.project
|
||||
.topics
|
||||
.iter()
|
||||
.map(|(name, _)| ListItem::new(name.clone()))
|
||||
.collect();
|
||||
let topics_list = List::new(topics)
|
||||
.block(Block::default().borders(Borders::ALL).title("Topics"))
|
||||
.highlight_style(Style::default().bg(Color::Blue.into()));
|
||||
f.render_stateful_widget(topics_list, chunks[1], &mut self.topics_list_state);
|
||||
|
||||
let requirements: Vec<ListItem> = self
|
||||
.project
|
||||
.topics
|
||||
.first()
|
||||
.unwrap()
|
||||
.1
|
||||
.requirements
|
||||
.iter()
|
||||
.map(|(name, _)| ListItem::new(name.clone()))
|
||||
.collect();
|
||||
let requirements_list = List::new(requirements)
|
||||
.block(Block::default().borders(Borders::ALL).title("Requirements"))
|
||||
.highlight_style(Style::default().bg(Color::Yellow.into()));
|
||||
f.render_stateful_widget(
|
||||
requirements_list,
|
||||
chunks[2],
|
||||
&mut self.requirements_list_state,
|
||||
);
|
||||
}
|
||||
|
||||
fn next_topic(&mut self) {
|
||||
let n = self.project.topics.len();
|
||||
if let Some(i) = self.topics_list_state.selected() {
|
||||
if i >= n - 1 {
|
||||
self.topics_list_state.select(Some(0));
|
||||
} else {
|
||||
self.topics_list_state.select(Some(i + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn previous_topic(&mut self) {
|
||||
let n = self.project.topics.len();
|
||||
if let Some(i) = self.topics_list_state.selected() {
|
||||
if i == 0 {
|
||||
self.topics_list_state.select(Some(n - 1));
|
||||
} else {
|
||||
self.topics_list_state.select(Some(i - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let project = demo_project();
|
||||
|
||||
let mut app = App::new(project);
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let tick_rate = Duration::from_millis(200);
|
||||
thread::spawn(move || {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if event::poll(timeout).expect("poll works") {
|
||||
if let CEvent::Key(key) = event::read().expect("read works") {
|
||||
tx.send(Event::Input(key)).expect("send works");
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
tx.send(Event::Tick).expect("tick works");
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| app.draw(f))?;
|
||||
|
||||
match rx.recv()? {
|
||||
Event::Input(event) => match event.code {
|
||||
KeyCode::Char('q') => {
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
terminal.show_cursor()?;
|
||||
break;
|
||||
}
|
||||
KeyCode::Down => {
|
||||
app.next_topic();
|
||||
}
|
||||
KeyCode::Up => {
|
||||
app.previous_topic();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -125,6 +125,7 @@ pub struct Project {
|
|||
serialize_with = "serialize_version",
|
||||
deserialize_with = "deserialize_version"
|
||||
)]
|
||||
#[schemars(with = "String", regex(pattern = r"^\d\.\d\.\d$"))]
|
||||
pub version: Version,
|
||||
#[serde(serialize_with = "my_trim")]
|
||||
pub description: String,
|
||||
|
|
@ -138,5 +139,5 @@ pub struct Project {
|
|||
|
||||
#[must_use]
|
||||
pub fn demo_project() -> Project {
|
||||
serde_yaml::from_str(include_str!("../req.yml")).expect("Should never happen!")
|
||||
serde_yaml::from_str(include_str!("../requirements.yml")).expect("Should never happen!")
|
||||
}
|
||||
|
|
|
|||
73
src/main.rs
73
src/main.rs
|
|
@ -1,6 +1,6 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
use clap::{CommandFactory, Parser, Subcommand};
|
||||
use indexmap::{
|
||||
map::{Keys, Values},
|
||||
IndexMap,
|
||||
|
|
@ -35,16 +35,28 @@ fn nl() -> String {
|
|||
|
||||
fn check_requirements(
|
||||
test_results: &str,
|
||||
output: &mut IndexMap<String, bool>,
|
||||
output: &mut IndexMap<String, (bool, Vec<String>)>,
|
||||
requirements: &IndexMap<String, Requirement>,
|
||||
allowed_requirements: &Regex,
|
||||
allowed_requirements: &[Regex],
|
||||
) {
|
||||
for (id, _) in dbg!(requirements) {
|
||||
if allowed_requirements.is_match(id) {
|
||||
if test_results.contains(&format!("{}: failed", id.trim())) {
|
||||
output.insert(id.trim().to_string(), false);
|
||||
for (id, _) in requirements {
|
||||
if allowed_requirements.iter().any(|r| r.is_match(id)) {
|
||||
let search_string = format!("{}: failed", id.trim());
|
||||
if test_results.contains(&search_string) {
|
||||
let errors = test_results.lines().filter_map(|l| {
|
||||
if l.starts_with(&search_string) {
|
||||
l.split_once(":")
|
||||
.map(|(_, txt)| txt)
|
||||
.and_then(|txt| txt.split_once("-").map(|(_, err)| err.to_string()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
output.insert(id.trim().to_string(), (false, errors.collect()));
|
||||
} else if test_results.contains(&format!("{}: passed", id.trim())) {
|
||||
output.entry(id.trim().to_string()).or_insert(true);
|
||||
output
|
||||
.entry(id.trim().to_string())
|
||||
.or_insert((true, Vec::new()));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -52,12 +64,12 @@ fn check_requirements(
|
|||
|
||||
fn has_valid_requirements(
|
||||
mut requirements: Keys<String, Requirement>,
|
||||
allowed_requirements: &Regex,
|
||||
allowed_requirements: &[Regex],
|
||||
) -> bool {
|
||||
requirements.any(|id| allowed_requirements.is_match(id))
|
||||
requirements.any(|id| allowed_requirements.iter().any(|r| r.is_match(id)))
|
||||
}
|
||||
|
||||
fn has_valid_topics(mut topics: Values<String, Topic>, allowed_requirements: &Regex) -> bool {
|
||||
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)
|
||||
|
|
@ -68,7 +80,7 @@ fn check_topics(
|
|||
test_results: &[PathBuf],
|
||||
output: &mut Vec<String>,
|
||||
topics: &IndexMap<String, Topic>,
|
||||
allowed_requirements: &Regex,
|
||||
allowed_requirements: &[Regex],
|
||||
level: usize,
|
||||
) -> anyhow::Result<()> {
|
||||
if !has_valid_topics(topics.values(), allowed_requirements) {
|
||||
|
|
@ -102,16 +114,19 @@ fn check_topics(
|
|||
|
||||
if !topic.requirements.is_empty() {
|
||||
for (id, req) in &topic.requirements {
|
||||
let status = if let Some(status) = test_status.get(id) {
|
||||
let (status, errors) = if let Some((status, errors)) = test_status.get(id) {
|
||||
if *status {
|
||||
":white_check_mark:"
|
||||
(":white_check_mark:", errors.to_owned())
|
||||
} else {
|
||||
":x:"
|
||||
(":x:", errors.to_owned())
|
||||
}
|
||||
} else {
|
||||
":warning:"
|
||||
(":warning:", Vec::new())
|
||||
};
|
||||
output.push(format!("- _{}_ - {}: {status}", id.trim(), req.name));
|
||||
for err in errors {
|
||||
output.push(format!(" - {}", err.trim()));
|
||||
}
|
||||
}
|
||||
|
||||
output.push(nl());
|
||||
|
|
@ -163,7 +178,7 @@ fn add_topics(output: &mut Vec<String>, topics: &IndexMap<String, Topic>, level:
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
/// Outputs the JSON schema for the input data
|
||||
Schema,
|
||||
|
|
@ -184,13 +199,19 @@ enum Command {
|
|||
Check {
|
||||
#[arg(short, long, default_value = "REQ-.*")]
|
||||
/// Regex to select which requirements should be checked
|
||||
allowed_requirements: String,
|
||||
allowed_requirements: Vec<String>,
|
||||
/// The path to the requirements file
|
||||
requirements: PathBuf,
|
||||
/// The path to the test output files
|
||||
#[arg(required=true, num_args=1..)]
|
||||
test_results: Vec<PathBuf>,
|
||||
},
|
||||
/// Generate shell completions
|
||||
Completions {
|
||||
/// The shell to generate the completions for
|
||||
#[arg(value_enum)]
|
||||
shell: clap_complete_command::Shell,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
|
|
@ -295,10 +316,14 @@ fn main() -> anyhow::Result<()> {
|
|||
}
|
||||
Command::Html { requirements } => {
|
||||
let output = to_markdown(requirements, false)?;
|
||||
let template = include_str!("../template.html");
|
||||
println!(
|
||||
"{}",
|
||||
markdown::to_html_with_options(&output, &markdown::Options::gfm())
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?
|
||||
template.replace(
|
||||
"{{content}}",
|
||||
&markdown::to_html_with_options(&output, &markdown::Options::gfm())
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?
|
||||
)
|
||||
);
|
||||
}
|
||||
Command::Schema => {
|
||||
|
|
@ -314,7 +339,10 @@ fn main() -> anyhow::Result<()> {
|
|||
requirements,
|
||||
test_results,
|
||||
} => {
|
||||
let re = Regex::new(&allowed_requirements).unwrap();
|
||||
let re = allowed_requirements
|
||||
.into_iter()
|
||||
.map(|r| Regex::new(&r).expect("Invalid regex!"));
|
||||
let re: Vec<_> = re.collect();
|
||||
let project: Project = parse(&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)?;
|
||||
|
|
@ -322,6 +350,9 @@ fn main() -> anyhow::Result<()> {
|
|||
let output = output.join("\n");
|
||||
println!("{output}");
|
||||
}
|
||||
Command::Completions { shell } => {
|
||||
shell.generate(&mut Args::command(), &mut std::io::stdout());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
<head>
|
||||
<style>
|
||||
/* General body styling */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
padding: 20px;
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/* Heading styles */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: #0056b3;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
border-bottom: 2px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #004494;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #003073;
|
||||
}
|
||||
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: #002652;
|
||||
font-style: italic;
|
||||
font-size: 1em;
|
||||
/* Ensures that subtopic headings don't scale down too much */
|
||||
font-weight: bold;
|
||||
/* Adds emphasis to make subtopic headings stand out */
|
||||
}
|
||||
|
||||
/* RFC keywords styling */
|
||||
p {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
strong em {
|
||||
color: #d63447;
|
||||
/* Bright red for MUST, SHOULD, etc. */
|
||||
font-style: normal;
|
||||
/* Override italic style from em */
|
||||
}
|
||||
|
||||
/* Link styling */
|
||||
a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* List styling for better readability */
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
ul li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
ul>li:not(:has(> p))::before {
|
||||
content: "• ";
|
||||
color: #007bff;
|
||||
/* Matching the link color */
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
ul>li:not(:has(> ul))>p::before {
|
||||
content: "• ";
|
||||
color: #007bff;
|
||||
/* Matching the link color */
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
/* Nested lists to indicate hierarchy */
|
||||
ul ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
/* Version and requirements emphasis */
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Special styling for configuration sections */
|
||||
h2 {
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
/* Config and Definitions section styling */
|
||||
ul ul li {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* Detailed requirement items */
|
||||
li strong em {
|
||||
display: inline-block;
|
||||
/* Ensures consistent alignment */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{content}}
|
||||
</body>
|
||||
Loading…
Reference in New Issue