Skip to main content

icydb_core/visitor/
context.rs

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