Skip to main content

p47h_engine/
validation.rs

1use crate::types::PolicyDiagnostic;
2use core_policy::Policy;
3use wasm_bindgen::prelude::*;
4
5/// Validates a policy TOML string
6///
7/// Checks if the provided string is a valid P47H policy.
8/// Returns Ok(()) if valid, or an error message if invalid.
9#[wasm_bindgen]
10pub fn validate_policy(policy_toml: &str) -> Result<(), JsValue> {
11    let _: Policy = toml::from_str(policy_toml)
12        .map_err(|e| JsValue::from_str(&format!("Invalid policy TOML: {}", e)))?;
13    Ok(())
14}
15
16/// Validates a policy TOML string with precise error reporting
17#[wasm_bindgen]
18pub fn validate_policy_detailed(policy_toml: &str) -> Result<JsValue, JsValue> {
19    // 1. Syntax Validation (TOML)
20    let policy: Policy = match toml::from_str(policy_toml) {
21        Ok(p) => p,
22        Err(e) => {
23            // Extract line/col from toml error string if possible
24            // toml error format: "TOML parse error at line 1, column 10"
25            let msg = e.to_string();
26            let (line, col) = parse_toml_error_position(&msg);
27
28            let diagnostic = PolicyDiagnostic {
29                valid: false,
30                message: Some(msg),
31                line,
32                column: col,
33            };
34            return Ok(serde_wasm_bindgen::to_value(&diagnostic)?);
35        }
36    };
37
38    // 2. Semantic Validation (Logic)
39    if policy.rules().is_empty() {
40        let diagnostic = PolicyDiagnostic {
41            valid: false,
42            message: Some("Policy is empty: must contain at least one rule".to_string()),
43            line: Some(0), // Header issue
44            column: None,
45        };
46        return Ok(serde_wasm_bindgen::to_value(&diagnostic)?);
47    }
48
49    // 3. Resource Validation
50    for (i, rule) in policy.rules().iter().enumerate() {
51        if rule.peer_id.is_empty() {
52            let diagnostic = PolicyDiagnostic {
53                valid: false,
54                message: Some(format!("Rule #{} has empty peer_id", i + 1)),
55                line: None, // Hard to map back to TOML line without a span-aware parser
56                column: None,
57            };
58            return Ok(serde_wasm_bindgen::to_value(&diagnostic)?);
59        }
60    }
61
62    // Success
63    let diagnostic = PolicyDiagnostic {
64        valid: true,
65        message: None,
66        line: None,
67        column: None,
68    };
69    Ok(serde_wasm_bindgen::to_value(&diagnostic)?)
70}
71
72// Helper to extract line info from toml error string (simple regex heuristic)
73fn parse_toml_error_position(msg: &str) -> (Option<u32>, Option<u32>) {
74    // Example: "TOML parse error at line 1, column 10"
75    // Regex: line (\d+), column (\d+)
76
77    if let Some(line_idx) = msg.find("line ") {
78        if let Some(col_idx) = msg.find("column ") {
79            let line_str = &msg[line_idx + 5..];
80            let line_end = line_str.find(',').unwrap_or(line_str.len());
81            let line = line_str[..line_end].trim().parse::<u32>().ok();
82
83            let col_str = &msg[col_idx + 7..];
84            let col_end = col_str
85                .find(|c: char| !c.is_numeric())
86                .unwrap_or(col_str.len());
87            let col = col_str[..col_end].trim().parse::<u32>().ok();
88
89            return (line, col);
90        }
91    }
92    (None, None)
93}