Skip to main content

xarf/
parser.rs

1//! High-level `parse()` entry point.
2//!
3//! Steps in order:
4//!
5//! 1. JSON-decode the input (string → `Value`, or accept a pre-built `Value`).
6//! 2. If it looks like a v3 report, run the v3→v4 conversion and add a
7//!    deprecation warning.
8//! 3. Validate against the bundled v4 schema (master + type-specific).
9//! 4. In non-strict mode, attempt to deserialize the data into a typed
10//!    [`Report`] even when validation surfaced issues — callers can inspect
11//!    `errors` independently.
12
13use serde_json::Value;
14
15use crate::error::{Result, ValidationError, ValidationInfo, ValidationWarning, XarfError};
16use crate::model::Report;
17use crate::v3_compat;
18use crate::validator::{ValidateOptions, validate};
19
20/// Outcome of [`parse`] / [`parse_value`].
21///
22/// - `report` is `Some` when serde could materialize a typed [`Report`].
23/// - `errors` lists every schema or business-rule violation discovered. The
24///   list may be non-empty even when `report` is `Some`: lossy parses surface
25///   the typed view *and* the errors, leaving the decision to the caller.
26#[derive(Debug, Clone)]
27pub struct ParseResult {
28    pub report: Option<Report>,
29    pub errors: Vec<ValidationError>,
30    pub warnings: Vec<ValidationWarning>,
31    pub info: Option<Vec<ValidationInfo>>,
32}
33
34/// Options accepted by [`parse`] and [`parse_value`].
35#[derive(Debug, Clone, Copy, Default)]
36pub struct ParseOptions {
37    /// Strict-mode validation; see [`crate::ValidateOptions::strict`].
38    pub strict: bool,
39    /// Surface missing optional/recommended fields in
40    /// [`ParseResult::info`].
41    pub show_missing_optional: bool,
42}
43
44impl From<ParseOptions> for ValidateOptions {
45    fn from(o: ParseOptions) -> Self {
46        Self {
47            strict: o.strict,
48            show_missing_optional: o.show_missing_optional,
49        }
50    }
51}
52
53/// Parse a XARF report from a JSON string.
54///
55/// Returns `Err(XarfError::InvalidJson)` only when the input is not valid
56/// JSON (or not a JSON object). All other failures surface as entries in
57/// [`ParseResult::errors`] / [`ParseResult::warnings`].
58pub fn parse(json: &str) -> Result<ParseResult> {
59    parse_with_options(json, ParseOptions::default())
60}
61
62/// Parse a XARF report from a JSON string with explicit [`ParseOptions`].
63pub fn parse_with_options(json: &str, options: ParseOptions) -> Result<ParseResult> {
64    let value: Value =
65        serde_json::from_str(json).map_err(|e| XarfError::InvalidJson(format!("{e}")))?;
66    if !value.is_object() {
67        return Err(XarfError::InvalidJson(format!(
68            "expected a JSON object, got {}",
69            value_type_name(&value)
70        )));
71    }
72    parse_value(value, options)
73}
74
75/// Parse a XARF report from a pre-decoded [`serde_json::Value`].
76pub fn parse_value(mut value: Value, options: ParseOptions) -> Result<ParseResult> {
77    let mut warnings: Vec<ValidationWarning> = Vec::new();
78
79    // ------------------------------------------------------------------
80    // Step 1 — v3 detection + conversion
81    // ------------------------------------------------------------------
82    if v3_compat::is_v3_report(&value) {
83        let mut conversion_msgs: Vec<String> = Vec::new();
84        value = v3_compat::convert_v3_to_v4(value, &mut conversion_msgs)?;
85        warnings.push(ValidationWarning::new("", v3_compat::deprecation_warning()));
86        warnings.extend(
87            conversion_msgs
88                .into_iter()
89                .map(|m| ValidationWarning::new("", m)),
90        );
91    }
92
93    // ------------------------------------------------------------------
94    // Step 2 — Schema validation
95    // ------------------------------------------------------------------
96    let validation = validate(&value, options.into())?;
97    let mut errors = validation.errors;
98    warnings.extend(validation.warnings);
99
100    // ------------------------------------------------------------------
101    // Step 3 — Strict-mode early return if validation already failed
102    // ------------------------------------------------------------------
103    if options.strict && !errors.is_empty() {
104        return Ok(ParseResult {
105            report: None,
106            errors,
107            warnings,
108            info: validation.info,
109        });
110    }
111
112    // ------------------------------------------------------------------
113    // Step 4 — Typed deserialisation (best effort in non-strict mode)
114    // ------------------------------------------------------------------
115    let report = match serde_json::from_value::<Report>(value) {
116        Ok(r) => Some(r),
117        Err(e) => {
118            // Attach the deserialization failure as an error so callers see
119            // why `report` is `None`.
120            errors.push(ValidationError::new("", format!("deserialize: {e}")));
121            None
122        }
123    };
124
125    Ok(ParseResult {
126        report,
127        errors,
128        warnings,
129        info: validation.info,
130    })
131}
132
133fn value_type_name(v: &Value) -> &'static str {
134    match v {
135        Value::Null => "null",
136        Value::Bool(_) => "boolean",
137        Value::Number(_) => "number",
138        Value::String(_) => "string",
139        Value::Array(_) => "array",
140        Value::Object(_) => "object",
141    }
142}