qubit_validator/binding/violation_draft.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//! Rule-local violations before a bound validator assigns its identity.
10
11use std::collections::BTreeMap;
12use std::fmt::Debug;
13use std::fmt::Formatter;
14use std::fmt::Result as FmtResult;
15
16use crate::ValidationPath;
17use crate::ViolationCode;
18use crate::ViolationParam;
19
20/// A structured violation draft without a rule identity.
21///
22/// # Examples
23///
24/// ```
25/// use qubit_validator::{ViolationCode, ViolationDraft, ViolationParam};
26///
27/// let draft = ViolationDraft::new(ViolationCode::new("text.too_short"))
28/// .with_param("minimum", ViolationParam::Unsigned(3));
29/// assert!(format!("{draft:?}").contains("text.too_short"));
30/// assert!(!format!("{draft:?}").contains("secret input"));
31/// ```
32#[must_use]
33#[derive(Eq, PartialEq)]
34pub struct ViolationDraft {
35 /// Stable, program-declared violation code.
36 code: ViolationCode,
37 /// Structured location of the violation.
38 path: ValidationPath,
39 /// Structured parameters keyed by program-declared names.
40 params: BTreeMap<&'static str, ViolationParam>,
41}
42impl ViolationDraft {
43 /// Creates a draft at the root path using a stable violation code.
44 ///
45 /// # Parameters
46 /// - `code`: Program-declared code that identifies the validation failure.
47 ///
48 /// # Returns
49 /// A draft with no parameters and an empty root path.
50 #[inline]
51 pub fn new(code: ViolationCode) -> Self {
52 Self {
53 code,
54 path: ValidationPath::root(),
55 params: BTreeMap::new(),
56 }
57 }
58 /// Replaces the relative path attached to this violation draft.
59 ///
60 /// # Parameters
61 /// - `path`: Structured location relative to the current validation target.
62 ///
63 /// # Returns
64 /// The draft with `path` stored for later report prefixing.
65 #[inline]
66 pub fn with_path(mut self, path: ValidationPath) -> Self {
67 self.path = path;
68 self
69 }
70 /// Adds or replaces a structured parameter by its declared name.
71 ///
72 /// The type restricts representation but cannot prove provenance. Supply
73 /// only program-declared tokens or rule configuration values, never
74 /// rejected input or sensitive values derived from it.
75 ///
76 /// # Parameters
77 /// - `name`: Static name declared by the application for this parameter.
78 /// - `value`: Structured value to associate with `name`, subject to the
79 /// caller's provenance and redaction responsibility.
80 ///
81 /// # Returns
82 /// The draft with the parameter inserted; an existing value with the same
83 /// name is replaced.
84 pub fn with_param(mut self, name: &'static str, value: ViolationParam) -> Self {
85 self.params.insert(name, value);
86 self
87 }
88 /// Splits the draft into safe components for final violation construction.
89 ///
90 /// The returned values are consumed by the binding layer, which supplies
91 /// the stable rule identity before exposing a [`crate::Violation`].
92 ///
93 /// # Returns
94 ///
95 /// The violation code, relative path, and structured parameters, in that
96 /// order.
97 #[inline]
98 pub(crate) fn parts(self) -> (ViolationCode, ValidationPath, BTreeMap<&'static str, ViolationParam>) {
99 (self.code, self.path, self.params)
100 }
101}
102
103impl Debug for ViolationDraft {
104 /// Formats only safe metadata and the parameter count.
105 ///
106 /// # Parameters
107 /// - `formatter`: Destination that receives the debug representation.
108 ///
109 /// # Returns
110 /// The result of writing the representation to `formatter`.
111 ///
112 /// # Errors
113 /// Returns the formatter's error if writing the representation fails.
114 fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
115 formatter
116 .debug_struct("ViolationDraft")
117 .field("code", &self.code)
118 .field("path", &self.path)
119 .field("parameter_count", &self.params.len())
120 .finish()
121 }
122}