Skip to content

Commit 7f22474

Browse files
committed
Cleanup of validation API
1 parent 14d3eb7 commit 7f22474

File tree

4 files changed

+107
-103
lines changed

4 files changed

+107
-103
lines changed

csaf-rs/src/csaf/validation.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@ impl std::fmt::Display for ValidationError {
2020
}
2121
}
2222

23+
/// Result of a CSAF validation
24+
#[derive(Debug, Clone, Serialize, Deserialize)]
25+
#[serde(rename_all = "camelCase")]
26+
pub struct ValidationResult {
27+
/// Whether the validation was successful (no errors)
28+
pub success: bool,
29+
/// The detected CSAF version
30+
pub version: String,
31+
/// List of validation errors (empty if successful)
32+
pub errors: Vec<ValidationError>,
33+
/// The validation preset that was used
34+
pub preset: ValidationPreset,
35+
}
36+
2337
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
2438
#[serde(rename_all = "lowercase")]
2539
pub enum ValidationPreset {
@@ -110,3 +124,70 @@ pub fn validate_by_test<VersionedDocument>(
110124
println!("Test with ID {} is missing implementation", test_id);
111125
}
112126
}
127+
128+
/// Collect validation errors for a given preset without printing
129+
pub fn collect_validation_errors<VersionedDocument>(
130+
target: &impl Validatable<VersionedDocument>,
131+
preset: &ValidationPreset,
132+
) -> Vec<ValidationError> {
133+
let mut errors = Vec::new();
134+
135+
if let Some(test_ids) = target.presets().get(preset) {
136+
let tests = target.tests();
137+
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+
}
145+
}
146+
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+
158+
ValidationResult {
159+
success: errors.is_empty(),
160+
version: version.to_string(),
161+
errors,
162+
preset,
163+
}
164+
}
165+
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>(
187+
target: &impl Validatable<VersionedDocument>,
188+
version: &str,
189+
preset: ValidationPreset,
190+
) {
191+
let result = create_validation_result(target, version, preset);
192+
print_validation_result(&result);
193+
}

csaf-rs/src/wasm.rs

Lines changed: 5 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,10 @@
33
//! This module provides WebAssembly bindings for validating CSAF documents in the browser.
44
55
use wasm_bindgen::prelude::*;
6-
use serde::{Deserialize, Serialize};
7-
use crate::csaf::validation::{ValidationError, ValidationPreset, Validatable};
6+
use crate::csaf::validation::{ValidationResult, ValidationPreset, create_validation_result};
87
use crate::csaf::csaf2_0::loader::load_document_from_str as load_document_from_str_2_0;
98
use crate::csaf::csaf2_1::loader::load_document_from_str as load_document_from_str_2_1;
109

11-
/// Result of a CSAF validation
12-
#[derive(Serialize, Deserialize)]
13-
#[serde(rename_all = "camelCase")]
14-
pub struct ValidationResult {
15-
/// Whether the validation was successful (no errors)
16-
pub success: bool,
17-
/// The detected CSAF version
18-
pub version: String,
19-
/// List of validation errors (empty if successful)
20-
pub errors: Vec<ValidationError>,
21-
/// The validation preset that was used
22-
pub preset: ValidationPreset,
23-
}
24-
2510
/// Initialize panic hook for better error messages in the browser console
2611
#[wasm_bindgen(start)]
2712
pub fn init() {
@@ -76,82 +61,18 @@ pub fn validate_csaf(json_str: &str, preset_str: &str) -> Result<JsValue, JsValu
7661

7762
/// Validate a CSAF 2.0 document
7863
fn validate_2_0(json_str: &str, preset: ValidationPreset) -> Result<ValidationResult, String> {
79-
// Load the document
8064
let document = load_document_from_str_2_0(json_str)
8165
.map_err(|e| format!("Failed to load CSAF 2.0 document: {}", e))?;
82-
83-
// Collect errors by temporarily capturing output
84-
let errors = collect_validation_errors_2_0(&document, preset.clone());
85-
86-
Ok(ValidationResult {
87-
success: errors.is_empty(),
88-
version: "2.0".to_string(),
89-
errors,
90-
preset,
91-
})
66+
67+
Ok(create_validation_result(&document, "2.0", preset))
9268
}
9369

9470
/// Validate a CSAF 2.1 document
9571
fn validate_2_1(json_str: &str, preset: ValidationPreset) -> Result<ValidationResult, String> {
96-
// Load the document
9772
let document = load_document_from_str_2_1(json_str)
9873
.map_err(|e| format!("Failed to load CSAF 2.1 document: {}", e))?;
99-
100-
// Collect errors by temporarily capturing output
101-
let errors = collect_validation_errors_2_1(&document, preset.clone());
102-
103-
Ok(ValidationResult {
104-
success: errors.is_empty(),
105-
version: "2.1".to_string(),
106-
errors,
107-
preset,
108-
})
109-
}
110-
111-
/// Helper to collect validation errors from CSAF 2.0 validation
112-
fn collect_validation_errors_2_0(
113-
target: &impl Validatable<crate::csaf::csaf2_0::schema::CommonSecurityAdvisoryFramework>,
114-
preset: ValidationPreset,
115-
) -> Vec<ValidationError> {
116-
let mut errors = Vec::new();
117-
118-
// Get tests for the preset
119-
if let Some(test_ids) = target.presets().get(&preset) {
120-
let tests = target.tests();
121-
122-
for test_id in test_ids {
123-
if let Some(test_fn) = tests.get(test_id) {
124-
if let Err(error) = test_fn(target.doc()) {
125-
errors.push(error);
126-
}
127-
}
128-
}
129-
}
130-
131-
errors
132-
}
133-
134-
/// Helper to collect validation errors from CSAF 2.1 validation
135-
fn collect_validation_errors_2_1(
136-
target: &impl Validatable<crate::csaf::csaf2_1::schema::CommonSecurityAdvisoryFramework>,
137-
preset: ValidationPreset,
138-
) -> Vec<ValidationError> {
139-
let mut errors = Vec::new();
140-
141-
// Get tests for the preset
142-
if let Some(test_ids) = target.presets().get(&preset) {
143-
let tests = target.tests();
144-
145-
for test_id in test_ids {
146-
if let Some(test_fn) = tests.get(test_id) {
147-
if let Err(error) = test_fn(target.doc()) {
148-
errors.push(error);
149-
}
150-
}
151-
}
152-
}
153-
154-
errors
74+
75+
Ok(create_validation_result(&document, "2.1", preset))
15576
}
15677

15778
#[cfg(test)]

csaf-validator/src/main.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::str::FromStr;
22
use anyhow::{bail, Result};
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::{validate_by_preset, validate_by_test, Validatable, ValidationPreset};
5+
use csaf_rs::csaf::validation::{create_validation_result, print_validation_result, validate_by_test, ValidationPreset};
66
use clap::Parser;
77

88
#[cfg(feature = "web")]
@@ -74,18 +74,20 @@ fn main() -> Result<()> {
7474
fn validate_file(path: &str, args: &Args) -> Result<()> {
7575
match args.csaf_version.as_str() {
7676
"2.0" => {
77-
process_document(load_document_2_0(path)?, args)
77+
let document = load_document_2_0(path)?;
78+
process_document(document, "2.0", args)
7879
}
7980
"2.1" => {
80-
process_document(load_document_2_1(path)?, args)
81+
let document = load_document_2_1(path)?;
82+
process_document(document, "2.1", args)
8183
}
8284
_ => bail!(format!("Invalid CSAF version: {}", args.csaf_version)),
8385
}
8486
}
8587

86-
fn process_document<T>(document: T, args: &Args) -> Result<()>
88+
fn process_document<T>(document: T, version: &str, args: &Args) -> Result<()>
8789
where
88-
T: Validatable<T>,
90+
T: csaf_rs::csaf::validation::Validatable<T>,
8991
{
9092
if !args.test_id.is_empty() {
9193
for test_id in &args.test_id {
@@ -94,11 +96,11 @@ where
9496
}
9597
Ok(())
9698
} else {
97-
let preset = match ValidationPreset::from_str(args.preset.as_str()) {
98-
Ok(preset) => preset,
99-
Err(_) => bail!(format!("Invalid validation preset: {}", args.preset)),
100-
};
101-
validate_by_preset(&document, preset);
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);
102104
Ok(())
103105
}
104106
}

csaf-validator/src/web/mod.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ struct StaticAssets;
2121
pub async fn start_server(host: &str, port: u16) -> anyhow::Result<()> {
2222
let addr: SocketAddr = format!("{}:{}", host, port).parse()?;
2323

24-
// Build the router - serves all static files
24+
// Build the router - serve all static files with fallback to index.html
2525
let app = Router::new()
26-
.route("/", get(index_handler))
26+
.route("/", get(static_handler))
2727
.route("/*path", get(static_handler));
2828

2929
println!("\n🚀 CSAF Validator Web UI starting...");
@@ -38,15 +38,15 @@ pub async fn start_server(host: &str, port: u16) -> anyhow::Result<()> {
3838
Ok(())
3939
}
4040

41-
/// Handler for the index page
42-
async fn index_handler() -> impl IntoResponse {
43-
static_handler(axum::extract::Path("index.html".to_string())).await
44-
}
45-
4641
/// Handler for all static files (HTML, JS, CSS, WASM, etc.)
47-
async fn static_handler(axum::extract::Path(path): axum::extract::Path<String>) -> Response {
48-
// Remove leading slash if present
42+
/// Defaults to index.html for root path
43+
async fn static_handler(path: Option<axum::extract::Path<String>>) -> Response {
44+
let path = path
45+
.map(|p| p.0)
46+
.unwrap_or_else(|| "index.html".to_string());
47+
4948
let path = path.trim_start_matches('/');
49+
let path = if path.is_empty() { "index.html" } else { path };
5050

5151
match StaticAssets::get(path) {
5252
Some(content) => {

0 commit comments

Comments
 (0)