Skip to main content

qubit_validator/report/
violation.rs

1// =============================================================================
2//    Copyright (c) 2025 - 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8
9//! Structured data violations.
10
11use std::collections::BTreeMap;
12use std::fmt::Debug;
13use std::fmt::Display;
14use std::fmt::Formatter;
15use std::fmt::Result as FmtResult;
16
17use super::ValidationPath;
18use super::ViolationCode;
19use super::ViolationParam;
20use crate::ValidatorId;
21
22/// One rule violation without the rejected value.
23///
24/// # Examples
25///
26/// ```
27/// use qubit_validator::{ValidatorId, Violation, ViolationCode};
28///
29/// let violation = Violation::new(
30///     ValidatorId::new("text.required"),
31///     ViolationCode::new("text.blank"),
32/// );
33/// assert_eq!(violation.code().as_str(), "text.blank");
34/// ```
35#[must_use]
36#[derive(Clone, Eq, PartialEq)]
37pub struct Violation {
38    /// Stable identifier of the rule which rejected the value.
39    rule_id: ValidatorId,
40    /// Stable, program-declared violation code.
41    code: ViolationCode,
42    /// Structured location of the rejected value.
43    path: ValidationPath,
44    /// Structured parameters whose provenance must be checked by their caller.
45    params: BTreeMap<&'static str, ViolationParam>,
46}
47
48impl Violation {
49    /// Creates a safe violation at the root path.
50    ///
51    /// # Parameters
52    ///
53    /// - `rule_id`: Stable identifier of the rule that rejected the value.
54    /// - `code`: Program-declared code describing the violation.
55    ///
56    /// # Returns
57    ///
58    /// A violation with an empty path and no parameters.
59    #[inline]
60    pub fn new(rule_id: ValidatorId, code: ViolationCode) -> Self {
61        Self {
62            rule_id,
63            code,
64            path: ValidationPath::root(),
65            params: BTreeMap::new(),
66        }
67    }
68
69    /// Returns the stable rule identifier.
70    ///
71    /// # Returns
72    ///
73    /// The identifier assigned by the bound validator.
74    #[must_use]
75    #[inline]
76    pub const fn rule_id(&self) -> ValidatorId {
77        self.rule_id
78    }
79
80    /// Returns the stable violation code.
81    ///
82    /// # Returns
83    ///
84    /// The program-declared code for this violation.
85    #[must_use]
86    #[inline]
87    pub const fn code(&self) -> ViolationCode {
88        self.code
89    }
90
91    /// Returns the structured path.
92    ///
93    /// The path contains no raw rejected value or map key.
94    ///
95    /// # Returns
96    ///
97    /// The structured path to the rejected value.
98    #[must_use]
99    #[inline]
100    pub const fn path(&self) -> &ValidationPath {
101        &self.path
102    }
103
104    /// Returns structured parameters supplied by the caller.
105    ///
106    /// The type restricts their representation but does not prove provenance;
107    /// callers remain responsible for keeping rejected input and sensitive
108    /// values derived from it out of this collection.
109    ///
110    /// # Returns
111    ///
112    /// The named parameters associated with this violation.
113    #[must_use]
114    #[inline]
115    pub const fn params(&self) -> &BTreeMap<&'static str, ViolationParam> {
116        &self.params
117    }
118
119    /// Replaces the structured location of the rejected value.
120    ///
121    /// # Parameters
122    ///
123    /// - `path`: Path containing only declared fields and opaque positions.
124    ///
125    /// # Returns
126    ///
127    /// The violation with `path` stored in place of its previous path.
128    #[inline]
129    pub fn with_path(mut self, path: ValidationPath) -> Self {
130        self.path = path;
131        self
132    }
133
134    /// Adds or replaces a structured parameter by its declared name.
135    ///
136    /// The type restricts representation but cannot prove provenance. Supply
137    /// only program-declared tokens or rule configuration values, never
138    /// rejected input or sensitive values derived from it.
139    ///
140    /// # Parameters
141    ///
142    /// - `name`: Static program-declared parameter name.
143    /// - `value`: Scalar or static token associated with the name, subject to
144    ///   the caller's provenance and redaction responsibility.
145    ///
146    /// # Returns
147    ///
148    /// The violation with the named parameter inserted or replaced.
149    pub fn with_param(mut self, name: &'static str, value: ViolationParam) -> Self {
150        self.params.insert(name, value);
151        self
152    }
153
154    /// Converts an adapter-produced draft into a violation for its bound rule.
155    ///
156    /// The draft is consumed so its code, path, and parameters can be moved
157    /// into the violation without cloning. The supplied identifier comes from
158    /// the validator binding that owns the draft.
159    ///
160    /// # Parameters
161    ///
162    /// - `rule_id`: Identifier of the validator that owns the draft.
163    /// - `draft`: Adapter-produced violation data consumed by this conversion.
164    ///
165    /// # Returns
166    ///
167    /// A violation associated with `rule_id` and containing the draft data.
168    pub(crate) fn from_draft(rule_id: ValidatorId, draft: crate::ViolationDraft) -> Self {
169        let (code, path, params) = draft.parts();
170        Self {
171            rule_id,
172            code,
173            path,
174            params,
175        }
176    }
177}
178
179impl Debug for Violation {
180    /// Formats stable identifiers without exposing path labels or parameters.
181    ///
182    /// # Parameters
183    ///
184    /// - `formatter`: Destination and formatting options for the debug output.
185    fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
186        formatter
187            .debug_struct("Violation")
188            .field("rule_id", &self.rule_id)
189            .field("code", &self.code)
190            .finish_non_exhaustive()
191    }
192}
193
194impl Display for Violation {
195    /// Formats only the stable violation code.
196    ///
197    /// # Parameters
198    ///
199    /// - `formatter`: Destination and formatting options for the display
200    ///   output.
201    fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
202        Display::fmt(&self.code, formatter)
203    }
204}