Skip to main content

icydb_model/visitor/
context.rs

1//! Module: visitor::context
2//! Responsibility: visitor issue-reporting context and path scoping helpers.
3//! Does not own: concrete normalize/validate traversal behavior.
4//! Boundary: shared diagnostics context passed through visitor entrypoints.
5
6use std::fmt;
7
8/// Application operation that produced visitor diagnostics.
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum ApplicationOperation {
11    /// Explicit mutable normalization traversal.
12    Normalize,
13    /// Explicit read-only validation traversal.
14    Validate,
15}
16
17impl fmt::Display for ApplicationOperation {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        match self {
20            Self::Normalize => f.write_str("normalization"),
21            Self::Validate => f.write_str("validation"),
22        }
23    }
24}
25
26/// Exact generated or application callback class that reported an issue.
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub enum CallbackKind {
29    /// Generated node-local normalizer traversal.
30    NormalizeAuto,
31    /// Application-authored node-local normalization hook.
32    NormalizeCustom,
33    /// One declared normalizer attachment.
34    Normalizer,
35    /// Generated node-local validator traversal.
36    ValidateAuto,
37    /// Application-authored node-local validation hook.
38    ValidateCustom,
39    /// One declared validator attachment.
40    Validator,
41}
42
43/// Typed identity of the callback that reported an application issue.
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub struct CallbackIdentity {
46    kind: CallbackKind,
47    type_path: &'static str,
48}
49
50impl CallbackIdentity {
51    /// Construct an identity from its callback class and concrete Rust type.
52    #[must_use]
53    pub const fn new(kind: CallbackKind, type_path: &'static str) -> Self {
54        Self { kind, type_path }
55    }
56
57    /// Return the callback class.
58    #[must_use]
59    pub const fn kind(&self) -> CallbackKind {
60        self.kind
61    }
62
63    /// Return the concrete Rust callback or application-value type path.
64    #[must_use]
65    pub const fn type_path(&self) -> &str {
66        self.type_path
67    }
68}
69
70impl fmt::Display for CallbackIdentity {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        write!(f, "{:?} {}", self.kind, self.type_path)
73    }
74}
75
76///
77/// VisitorContext
78///
79/// Narrow interface exposed to visitors for reporting non-fatal issues.
80/// Implemented by adapters via a short-lived context object.
81///
82
83pub trait VisitorContext {
84    fn add_issue(&mut self, issue: Issue);
85    fn add_issue_at(&mut self, seg: PathSegment, issue: Issue);
86}
87
88impl dyn VisitorContext + '_ {
89    pub fn issue(&mut self, issue: impl Into<Issue>) {
90        self.add_issue(issue.into());
91    }
92
93    pub fn issue_at(&mut self, seg: PathSegment, issue: impl Into<Issue>) {
94        self.add_issue_at(seg, issue.into());
95    }
96}
97
98/// VisitorContext that pins all issues to a single path segment.
99pub struct ScopedContext<'a> {
100    ctx: &'a mut dyn VisitorContext,
101    seg: PathSegment,
102}
103
104/// Visitor context that binds unowned issues to one typed callback identity.
105///
106/// Nested callback contexts preserve the innermost identity, allowing a
107/// declared normalizer or validator to override its generated traversal hook.
108pub struct CallbackContext<'a> {
109    ctx: &'a mut dyn VisitorContext,
110    callback: CallbackIdentity,
111}
112
113impl<'a> CallbackContext<'a> {
114    /// Bind subsequent issues to `callback` unless already more specifically
115    /// identified by a nested callback context.
116    #[must_use]
117    pub fn new(ctx: &'a mut dyn VisitorContext, callback: CallbackIdentity) -> Self {
118        Self { ctx, callback }
119    }
120}
121
122impl VisitorContext for CallbackContext<'_> {
123    fn add_issue(&mut self, mut issue: Issue) {
124        issue.bind_callback_if_unset(&self.callback);
125        self.ctx.add_issue(issue);
126    }
127
128    fn add_issue_at(&mut self, seg: PathSegment, mut issue: Issue) {
129        issue.bind_callback_if_unset(&self.callback);
130        self.ctx.add_issue_at(seg, issue);
131    }
132}
133
134impl<'a> ScopedContext<'a> {
135    #[must_use]
136    pub fn new(ctx: &'a mut dyn VisitorContext, seg: PathSegment) -> Self {
137        Self { ctx, seg }
138    }
139}
140
141impl VisitorContext for ScopedContext<'_> {
142    fn add_issue(&mut self, issue: Issue) {
143        self.ctx.add_issue_at(self.seg.clone(), issue);
144    }
145
146    fn add_issue_at(&mut self, _seg: PathSegment, issue: Issue) {
147        self.ctx.add_issue_at(self.seg.clone(), issue);
148    }
149}
150
151///
152/// Issue
153///
154
155#[derive(Clone, Debug, Default, Eq, PartialEq)]
156pub struct Issue {
157    callback: Option<CallbackIdentity>,
158    message: String,
159}
160
161impl Issue {
162    #[must_use]
163    pub fn new(message: impl Into<String>) -> Self {
164        Self {
165            callback: None,
166            message: message.into(),
167        }
168    }
169
170    /// Return the typed callback identity attached during traversal.
171    #[must_use]
172    pub const fn callback(&self) -> Option<&CallbackIdentity> {
173        self.callback.as_ref()
174    }
175
176    #[must_use]
177    pub fn message(&self) -> &str {
178        &self.message
179    }
180
181    #[must_use]
182    pub fn into_message(self) -> String {
183        self.message
184    }
185
186    const fn bind_callback_if_unset(&mut self, callback: &CallbackIdentity) {
187        if self.callback.is_none() {
188            self.callback = Some(*callback);
189        }
190    }
191}
192
193impl From<String> for Issue {
194    fn from(message: String) -> Self {
195        Self::new(message)
196    }
197}
198
199impl From<&str> for Issue {
200    fn from(message: &str) -> Self {
201        Self::new(message)
202    }
203}
204
205impl fmt::Display for Issue {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        if let Some(callback) = &self.callback {
208            write!(f, "{callback}: {}", self.message)
209        } else {
210            f.write_str(&self.message)
211        }
212    }
213}
214
215///
216/// PathSegment
217///
218
219#[derive(Clone, Debug)]
220pub enum PathSegment {
221    Empty,
222    Field(&'static str),
223    Index(usize),
224}
225
226impl From<&'static str> for PathSegment {
227    fn from(s: &'static str) -> Self {
228        Self::Field(s)
229    }
230}
231
232impl From<usize> for PathSegment {
233    fn from(i: usize) -> Self {
234        Self::Index(i)
235    }
236}
237
238impl From<Option<&'static str>> for PathSegment {
239    fn from(opt: Option<&'static str>) -> Self {
240        match opt {
241            Some(s) if !s.is_empty() => Self::Field(s),
242            _ => Self::Empty,
243        }
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::{CallbackIdentity, CallbackKind, Issue};
250
251    #[test]
252    fn custom_issue_preserves_message() {
253        let issue = Issue::from("pet name is reserved");
254
255        assert_eq!(issue.message(), "pet name is reserved");
256        assert_eq!(issue.callback(), None);
257        assert_eq!(issue.to_string(), "pet name is reserved");
258    }
259
260    #[test]
261    fn typed_issue_displays_callback_identity_without_changing_its_message() {
262        let mut issue = Issue::from("pet name is reserved");
263        issue.bind_callback_if_unset(&CallbackIdentity::new(
264            CallbackKind::Validator,
265            "schema::PetName",
266        ));
267
268        let callback = issue.callback().expect("callback identity should bind");
269        assert_eq!(callback.kind(), CallbackKind::Validator);
270        assert_eq!(callback.type_path(), "schema::PetName");
271        assert_eq!(issue.message(), "pet name is reserved");
272        assert_eq!(
273            issue.to_string(),
274            "Validator schema::PetName: pet name is reserved"
275        );
276    }
277}