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 fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
182 formatter
183 .debug_struct("Violation")
184 .field("rule_id", &self.rule_id)
185 .field("code", &self.code)
186 .finish_non_exhaustive()
187 }
188}
189
190impl Display for Violation {
191 /// Formats only the stable violation code.
192 fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
193 Display::fmt(&self.code, formatter)
194 }
195}