1use std::fmt;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7#[non_exhaustive]
8pub enum ValidationErrorKind {
9 InvalidValue,
11 StructuralLimit,
13 Unsupported,
15}
16
17#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct ValidationError {
20 pub kind: ValidationErrorKind,
22 pub path: String,
24 pub field: String,
26 pub message: String,
28 pub frame: Option<u32>,
30}
31
32impl ValidationError {
33 pub fn new(
35 kind: ValidationErrorKind,
36 path: impl Into<String>,
37 field: impl Into<String>,
38 message: impl Into<String>,
39 ) -> Self {
40 Self {
41 kind,
42 path: path.into(),
43 field: field.into(),
44 message: message.into(),
45 frame: None,
46 }
47 }
48
49 pub fn invalid(
51 path: impl Into<String>,
52 field: impl Into<String>,
53 message: impl Into<String>,
54 ) -> Self {
55 Self::new(ValidationErrorKind::InvalidValue, path, field, message)
56 }
57
58 pub fn limit(
60 path: impl Into<String>,
61 field: impl Into<String>,
62 message: impl Into<String>,
63 ) -> Self {
64 Self::new(ValidationErrorKind::StructuralLimit, path, field, message)
65 }
66
67 pub fn unsupported(
69 path: impl Into<String>,
70 field: impl Into<String>,
71 message: impl Into<String>,
72 ) -> Self {
73 Self::new(ValidationErrorKind::Unsupported, path, field, message)
74 }
75}
76
77impl fmt::Display for ValidationError {
78 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79 if let Some(frame) = self.frame {
80 write!(formatter, "frame {frame}: ")?;
81 }
82 write!(formatter, "{}.{}: {}", self.path, self.field, self.message)
83 }
84}
85
86impl std::error::Error for ValidationError {}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub struct ValidationLimits {
96 pub max_nodes: usize,
98 pub max_depth: usize,
100}
101
102impl Default for ValidationLimits {
103 fn default() -> Self {
104 Self {
105 max_nodes: 100_000,
106 max_depth: 64,
107 }
108 }
109}
110
111#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
113pub struct SceneStats {
114 pub nodes: usize,
117 pub max_depth: usize,
119}
120
121pub struct ValidationState {
123 limits: ValidationLimits,
124 pub stats: SceneStats,
126}
127
128impl ValidationState {
129 pub fn new(limits: ValidationLimits) -> Result<Self, ValidationError> {
131 if limits.max_nodes == 0
132 || limits.max_nodes > 1_000_000
133 || limits.max_depth == 0
134 || limits.max_depth > 128
135 {
136 return Err(ValidationError::new(
137 ValidationErrorKind::InvalidValue,
138 "scene",
139 "limits",
140 "Require 1..=1000000 nodes and 1..=128 levels",
141 ));
142 }
143 Ok(Self {
144 limits,
145 stats: SceneStats::default(),
146 })
147 }
148
149 pub fn enter(&mut self, path: &str, depth: usize) -> Result<(), ValidationError> {
151 self.enter_with(|| path.to_owned(), depth)
152 }
153
154 pub fn enter_with(
156 &mut self,
157 path: impl FnOnce() -> String,
158 depth: usize,
159 ) -> Result<(), ValidationError> {
160 if depth > self.limits.max_depth || self.stats.nodes >= self.limits.max_nodes {
161 let (field, allowed) = if depth > self.limits.max_depth {
162 ("max_depth", self.limits.max_depth)
163 } else {
164 ("max_nodes", self.limits.max_nodes)
165 };
166 return Err(ValidationError::limit(
167 path(),
168 field,
169 format!("Scene exceeds {field}={allowed}"),
170 ));
171 }
172 self.stats.nodes += 1;
173 self.stats.max_depth = self.stats.max_depth.max(depth);
174 Ok(())
175 }
176}