Skip to content

Commit 2662b5c

Browse files
committed
Major cleanup of validation API
1 parent 7f22474 commit 2662b5c

27 files changed

+173
-124
lines changed

csaf-rs/src/csaf/validation.rs

Lines changed: 85 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
use serde::{Deserialize, Serialize};
12
use std::collections::HashMap;
23
use std::fmt::{Display, Formatter};
34
use std::str::FromStr;
4-
use serde::{Serialize, Deserialize};
55

66
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
77
pub struct ValidationError {
@@ -20,6 +20,18 @@ impl std::fmt::Display for ValidationError {
2020
}
2121
}
2222

23+
/// Result of executing a single test
24+
#[derive(Debug, Clone, Serialize, Deserialize)]
25+
#[serde(rename_all = "camelCase")]
26+
pub struct TestResult {
27+
/// The test ID that was executed
28+
pub test_id: String,
29+
/// Whether the test passed
30+
pub success: bool,
31+
/// The errors if the test failed (empty if successful)
32+
pub errors: Vec<ValidationError>,
33+
}
34+
2335
/// Result of a CSAF validation
2436
#[derive(Debug, Clone, Serialize, Deserialize)]
2537
#[serde(rename_all = "camelCase")]
@@ -32,6 +44,8 @@ pub struct ValidationResult {
3244
pub errors: Vec<ValidationError>,
3345
/// The validation preset that was used
3446
pub preset: ValidationPreset,
47+
/// Individual test results with execution details
48+
pub test_results: Vec<TestResult>,
3549
}
3650

3751
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
@@ -66,11 +80,19 @@ impl Display for ValidationPreset {
6680
}
6781

6882
pub trait Validate {
69-
/// Validates this object according to a validation preset
70-
fn validate_preset(&'static self, preset: ValidationPreset);
71-
72-
/// Validates this object according to a specific test ID.
73-
fn validate_by_test(&self, version: &str);
83+
/// Validates this object according to
84+
fn validate_by_test<VersionedDocument>(&self, test_id: &str) -> TestResult;
85+
86+
/// Validates this object according to specific test IDs and returns detailed results
87+
fn validate_tests(
88+
&self,
89+
version: &str,
90+
preset: ValidationPreset,
91+
test_ids: &[&str],
92+
) -> ValidationResult;
93+
94+
/// Validates this object according to a validation preset and returns detailed results
95+
fn validate_preset(&self, version: &str, preset: ValidationPreset) -> ValidationResult;
7496
}
7597

7698
pub type Test<VersionedDocument> = fn(&VersionedDocument) -> Result<(), ValidationError>;
@@ -90,104 +112,85 @@ pub trait Validatable<VersionedDocument> {
90112
fn doc(&self) -> &VersionedDocument;
91113
}
92114

93-
/// Executes all tests of the specified [preset] against the [target]
94-
/// (which is of type [VersionedDocument], e.g. a CSAF 2.0 document).
95-
pub fn validate_by_preset<VersionedDocument>(
96-
target: &impl Validatable<VersionedDocument>,
97-
preset: ValidationPreset,
98-
) {
99-
println!("Validating document with {:?} preset... \n", preset);
100-
101-
// Loop through tests
102-
if let Some(tests) = target.presets().get(&preset) {
103-
for test_id in tests {
104-
println!("Executing Test {}... ", test_id);
105-
validate_by_test(target, test_id);
106-
107-
println!()
108-
}
109-
} else {
110-
println!("No tests found for preset")
111-
}
112-
}
113-
115+
/// Execute a single test and return the test result.
116+
///
117+
/// This function will check, whether the test_id exists in the Validatable's
118+
/// tests. If it does, it will execute the test function and return the result.
119+
/// If not, it will return a TestResult indicating that the test was not found.
114120
pub fn validate_by_test<VersionedDocument>(
115121
target: &impl Validatable<VersionedDocument>,
116122
test_id: &str,
117-
) {
118-
if let Some(test_fn) = target.tests().get(test_id) {
119-
let _ = match test_fn(target.doc()) {
120-
Ok(()) => println!("> Test Success"),
121-
Err(e) => println!("> Error: {}", e),
122-
};
123+
) -> TestResult {
124+
// Fetch tests from the validatable
125+
let tests = target.tests();
126+
127+
// Try to find and execute the test specified by the test_id
128+
if let Some(test_fn) = tests.get(test_id) {
129+
match test_fn(target.doc()) {
130+
Ok(()) => TestResult {
131+
test_id: test_id.to_string(),
132+
success: true,
133+
errors: vec![],
134+
},
135+
Err(error) => TestResult {
136+
test_id: test_id.to_string(),
137+
success: false,
138+
errors: vec![error],
139+
},
140+
}
123141
} else {
124-
println!("Test with ID {} is missing implementation", test_id);
142+
let error = ValidationError {
143+
message: format!("Test '{}' not found", test_id),
144+
instance_path: "".to_string(),
145+
};
146+
TestResult {
147+
test_id: test_id.to_string(),
148+
success: false,
149+
errors: vec![error],
150+
}
125151
}
126152
}
127153

128-
/// Collect validation errors for a given preset without printing
129-
pub fn collect_validation_errors<VersionedDocument>(
154+
/// Validate document with specific tests and return detailed results.
155+
pub fn validate_by_tests<VersionedDocument>(
130156
target: &impl Validatable<VersionedDocument>,
131-
preset: &ValidationPreset,
132-
) -> Vec<ValidationError> {
157+
version: &str,
158+
preset: ValidationPreset,
159+
test_ids: &[&str],
160+
) -> ValidationResult {
133161
let mut errors = Vec::new();
162+
let mut test_results = Vec::new();
134163

135-
if let Some(test_ids) = target.presets().get(preset) {
136-
let tests = target.tests();
164+
// Loop through tests and gather all results and errors
165+
for test_id in test_ids {
166+
let test_result = validate_by_test(target, test_id);
137167

138-
for test_id in test_ids {
139-
if let Some(test_fn) = tests.get(test_id) {
140-
if let Err(error) = test_fn(target.doc()) {
141-
errors.push(error);
142-
}
143-
}
144-
}
168+
errors.extend(test_result.errors.iter().cloned());
169+
test_results.push(test_result);
145170
}
146171

147-
errors
148-
}
149-
150-
/// Create a validation result from a validatable document
151-
pub fn create_validation_result<VersionedDocument>(
152-
target: &impl Validatable<VersionedDocument>,
153-
version: &str,
154-
preset: ValidationPreset,
155-
) -> ValidationResult {
156-
let errors = collect_validation_errors(target, &preset);
157-
158172
ValidationResult {
159173
success: errors.is_empty(),
160174
version: version.to_string(),
161175
errors,
162176
preset,
177+
test_results,
163178
}
164179
}
165180

166-
/// Print a validation result to stdout (for CLI use)
167-
pub fn print_validation_result(result: &ValidationResult) {
168-
println!("Validating document with {:?} preset... \n", result.preset);
169-
170-
if result.success {
171-
println!("✅ Validation passed! No errors found.");
172-
println!(" CSAF Version: {}", result.version);
173-
println!();
174-
} else {
175-
println!("❌ Validation failed with {} error(s):", result.errors.len());
176-
println!(" CSAF Version: {}", result.version);
177-
println!();
178-
for error in &result.errors {
179-
println!(" • {} (at {})", error.message, error.instance_path);
180-
}
181-
println!();
182-
}
183-
}
184-
185-
/// Validate and print results - convenience function for CLI use
186-
pub fn validate_and_print<VersionedDocument>(
181+
/// Validate document with a preset and return detailed results.
182+
pub fn validate_by_preset<VersionedDocument>(
187183
target: &impl Validatable<VersionedDocument>,
188184
version: &str,
189185
preset: ValidationPreset,
190-
) {
191-
let result = create_validation_result(target, version, preset);
192-
print_validation_result(&result);
186+
) -> ValidationResult {
187+
// Retrieve the test IDs for the given preset
188+
let test_ids: Vec<&str> = target
189+
.presets()
190+
.get(&preset)
191+
.map(|ids| ids.iter().copied().collect())
192+
.unwrap_or_default();
193+
194+
// Forward them to validate_by_tests
195+
validate_by_tests(target, version, preset, &test_ids)
193196
}

csaf-rs/src/wasm.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//! This module provides WebAssembly bindings for validating CSAF documents in the browser.
44
55
use wasm_bindgen::prelude::*;
6-
use crate::csaf::validation::{ValidationResult, ValidationPreset, create_validation_result};
6+
use crate::csaf::validation::{ValidationResult, ValidationPreset, validate_by_preset};
77
use crate::csaf::csaf2_0::loader::load_document_from_str as load_document_from_str_2_0;
88
use crate::csaf::csaf2_1::loader::load_document_from_str as load_document_from_str_2_1;
99

@@ -64,15 +64,15 @@ fn validate_2_0(json_str: &str, preset: ValidationPreset) -> Result<ValidationRe
6464
let document = load_document_from_str_2_0(json_str)
6565
.map_err(|e| format!("Failed to load CSAF 2.0 document: {}", e))?;
6666

67-
Ok(create_validation_result(&document, "2.0", preset))
67+
Ok(validate_by_preset(&document, "2.0", preset))
6868
}
6969

7070
/// Validate a CSAF 2.1 document
7171
fn validate_2_1(json_str: &str, preset: ValidationPreset) -> Result<ValidationResult, String> {
7272
let document = load_document_from_str_2_1(json_str)
7373
.map_err(|e| format!("Failed to load CSAF 2.1 document: {}", e))?;
7474

75-
Ok(create_validation_result(&document, "2.1", preset))
75+
Ok(validate_by_preset(&document, "2.1", preset))
7676
}
7777

7878
#[cfg(test)]
@@ -86,6 +86,7 @@ mod tests {
8686
version: "2.0".to_string(),
8787
errors: vec![],
8888
preset: ValidationPreset::Basic,
89+
test_results: vec![],
8990
};
9091

9192
let json = serde_json::to_string(&result).unwrap();

csaf-validator/src/main.rs

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
use std::str::FromStr;
21
use anyhow::{bail, Result};
2+
use clap::Parser;
33
use csaf_rs::csaf::csaf2_0::loader::load_document as load_document_2_0;
44
use csaf_rs::csaf::csaf2_1::loader::load_document as load_document_2_1;
5-
use csaf_rs::csaf::validation::{create_validation_result, print_validation_result, validate_by_test, ValidationPreset};
6-
use clap::Parser;
5+
use csaf_rs::csaf::validation::{
6+
validate_by_preset, validate_by_tests, ValidationPreset, ValidationResult,
7+
};
8+
use std::str::FromStr;
79

810
#[cfg(feature = "web")]
911
mod web;
@@ -55,7 +57,9 @@ async fn main() -> Result<()> {
5557
}
5658

5759
// Otherwise, validate a file
58-
let path = args.path.as_ref()
60+
let path = args
61+
.path
62+
.as_ref()
5963
.ok_or_else(|| anyhow::anyhow!("Path argument is required when not using --web"))?;
6064

6165
validate_file(path, &args)
@@ -65,42 +69,84 @@ async fn main() -> Result<()> {
6569
fn main() -> Result<()> {
6670
let args = Args::parse();
6771

68-
let path = args.path.as_ref()
72+
let path = args
73+
.path
74+
.as_ref()
6975
.ok_or_else(|| anyhow::anyhow!("Path argument is required"))?;
7076

7177
validate_file(path, &args)
7278
}
7379

80+
/// Try to validate a file as a CSAF document based on the specified version.
7481
fn validate_file(path: &str, args: &Args) -> Result<()> {
7582
match args.csaf_version.as_str() {
7683
"2.0" => {
7784
let document = load_document_2_0(path)?;
78-
process_document(document, "2.0", args)
85+
validate_document(document, "2.0", args)
7986
}
8087
"2.1" => {
8188
let document = load_document_2_1(path)?;
82-
process_document(document, "2.1", args)
89+
validate_document(document, "2.1", args)
8390
}
8491
_ => bail!(format!("Invalid CSAF version: {}", args.csaf_version)),
8592
}
8693
}
8794

88-
fn process_document<T>(document: T, version: &str, args: &Args) -> Result<()>
95+
/// Validate a CSAF document of the specified version with the provided arguments.
96+
///
97+
/// This prints the results of the tests on stdout.
98+
fn validate_document<T>(document: T, version: &str, args: &Args) -> Result<()>
8999
where
90100
T: csaf_rs::csaf::validation::Validatable<T>,
91101
{
92-
if !args.test_id.is_empty() {
93-
for test_id in &args.test_id {
94-
println!("\nExecuting Test {}... ", test_id);
95-
validate_by_test(&document, test_id.as_str());
102+
let preset = ValidationPreset::from_str(args.preset.as_str())
103+
.map_err(|_| anyhow::anyhow!("Invalid validation preset: {}", args.preset))?;
104+
105+
let result = if !args.test_id.is_empty() {
106+
// Individual test validation
107+
let test_ids: Vec<&str> = args.test_id.iter().map(|s| s.as_str()).collect();
108+
validate_by_tests(&document, version, preset, &test_ids)
109+
} else {
110+
// Preset validation
111+
validate_by_preset(&document, version, preset)
112+
};
113+
114+
print_validation_result(&result);
115+
Ok(())
116+
}
117+
118+
/// Print a validation result to stdout (for CLI use)
119+
pub fn print_validation_result(result: &ValidationResult) {
120+
println!("CSAF Version: {}", result.version);
121+
println!("Validating document with {:?} preset...\n", result.preset);
122+
123+
// Print individual test results
124+
for test_result in &result.test_results {
125+
if test_result.success {
126+
println!("Executing Test {}... ✅ Success", test_result.test_id);
127+
} else if let Some(error) = test_result.errors.first() {
128+
if error.message.contains("not found") {
129+
println!(
130+
"Executing Test {}... ⚠️ Test not found",
131+
test_result.test_id
132+
);
133+
} else {
134+
println!(
135+
"Executing Test {}... ❌ Error: {}",
136+
test_result.test_id, error.message
137+
);
138+
}
96139
}
97-
Ok(())
140+
}
141+
142+
// Print summary
143+
println!();
144+
if result.success {
145+
println!("✅ Validation passed! No errors found.\n");
98146
} else {
99-
let preset = ValidationPreset::from_str(args.preset.as_str())
100-
.map_err(|_| anyhow::anyhow!("Invalid validation preset: {}", args.preset))?;
101-
102-
let result = create_validation_result(&document, version, preset);
103-
print_validation_result(&result);
104-
Ok(())
147+
println!(
148+
"❌ Validation failed with {} error(s)\n",
149+
result.errors.len()
150+
);
105151
}
106152
}

0 commit comments

Comments
 (0)