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 serde::Deserialize;
7use std::fmt;
8
9///
10/// VisitorContext
11///
12/// Narrow interface exposed to visitors for reporting non-fatal issues.
13/// Implemented by adapters via a short-lived context object.
14///
15
16pub trait VisitorContext {
17    fn add_issue(&mut self, issue: Issue);
18    fn add_issue_at(&mut self, seg: PathSegment, issue: Issue);
19}
20
21impl dyn VisitorContext + '_ {
22    pub fn issue(&mut self, issue: impl Into<Issue>) {
23        self.add_issue(issue.into());
24    }
25
26    pub fn issue_at(&mut self, seg: PathSegment, issue: impl Into<Issue>) {
27        self.add_issue_at(seg, issue.into());
28    }
29}
30
31/// VisitorContext that pins all issues to a single path segment.
32pub struct ScopedContext<'a> {
33    ctx: &'a mut dyn VisitorContext,
34    seg: PathSegment,
35}
36
37impl<'a> ScopedContext<'a> {
38    #[must_use]
39    pub fn new(ctx: &'a mut dyn VisitorContext, seg: PathSegment) -> Self {
40        Self { ctx, seg }
41    }
42}
43
44impl VisitorContext for ScopedContext<'_> {
45    fn add_issue(&mut self, issue: Issue) {
46        self.ctx.add_issue_at(self.seg.clone(), issue);
47    }
48
49    fn add_issue_at(&mut self, _seg: PathSegment, issue: Issue) {
50        self.ctx.add_issue_at(self.seg.clone(), issue);
51    }
52}
53
54///
55/// Issue
56///
57
58#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
59pub struct Issue {
60    message: String,
61}
62
63impl Issue {
64    #[must_use]
65    pub fn new(message: impl Into<String>) -> Self {
66        Self {
67            message: message.into(),
68        }
69    }
70
71    #[must_use]
72    pub fn message(&self) -> &str {
73        &self.message
74    }
75
76    #[must_use]
77    pub fn into_message(self) -> String {
78        self.message
79    }
80}
81
82impl From<String> for Issue {
83    fn from(message: String) -> Self {
84        Self { message }
85    }
86}
87
88impl From<&str> for Issue {
89    fn from(message: &str) -> Self {
90        Self::new(message)
91    }
92}
93
94impl fmt::Display for Issue {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        f.write_str(&self.message)
97    }
98}
99
100///
101/// PathSegment
102///
103
104#[derive(Clone, Debug)]
105pub enum PathSegment {
106    Empty,
107    Field(&'static str),
108    Index(usize),
109}
110
111impl From<&'static str> for PathSegment {
112    fn from(s: &'static str) -> Self {
113        Self::Field(s)
114    }
115}
116
117impl From<usize> for PathSegment {
118    fn from(i: usize) -> Self {
119        Self::Index(i)
120    }
121}
122
123impl From<Option<&'static str>> for PathSegment {
124    fn from(opt: Option<&'static str>) -> Self {
125        match opt {
126            Some(s) if !s.is_empty() => Self::Field(s),
127            _ => Self::Empty,
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::Issue;
135
136    #[test]
137    fn custom_issue_preserves_message() {
138        let issue = Issue::from("pet name is reserved");
139
140        assert_eq!(issue.message(), "pet name is reserved");
141        assert_eq!(issue.to_string(), "pet name is reserved");
142    }
143}