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}