icydb_model/visitor/
context.rs1use std::fmt;
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum ApplicationOperation {
11 Normalize,
13 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub enum CallbackKind {
29 NormalizeAuto,
31 NormalizeCustom,
33 Normalizer,
35 ValidateAuto,
37 ValidateCustom,
39 Validator,
41}
42
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub struct CallbackIdentity {
46 kind: CallbackKind,
47 type_path: &'static str,
48}
49
50impl CallbackIdentity {
51 #[must_use]
53 pub const fn new(kind: CallbackKind, type_path: &'static str) -> Self {
54 Self { kind, type_path }
55 }
56
57 #[must_use]
59 pub const fn kind(&self) -> CallbackKind {
60 self.kind
61 }
62
63 #[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
76pub 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
98pub struct ScopedContext<'a> {
100 ctx: &'a mut dyn VisitorContext,
101 seg: PathSegment,
102}
103
104pub struct CallbackContext<'a> {
109 ctx: &'a mut dyn VisitorContext,
110 callback: CallbackIdentity,
111}
112
113impl<'a> CallbackContext<'a> {
114 #[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#[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 #[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#[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}