1use crate::id::{FeedId, StageId};
7
8#[derive(Debug, thiserror::Error)]
10pub enum NvError {
11 #[error("media error: {0}")]
13 Media(#[from] MediaError),
14
15 #[error("stage error: {0}")]
17 Stage(#[from] StageError),
18
19 #[error("temporal error: {0}")]
21 Temporal(#[from] TemporalError),
22
23 #[error("view error: {0}")]
25 View(#[from] ViewError),
26
27 #[error("runtime error: {0}")]
29 Runtime(#[from] RuntimeError),
30
31 #[error("config error: {0}")]
33 Config(#[from] ConfigError),
34}
35
36#[derive(Debug, Clone, thiserror::Error)]
46pub enum MediaError {
47 #[error("connection failed to `{redacted_url}`: {detail}", redacted_url = crate::security::redact_url(url))]
49 ConnectionFailed { url: String, detail: String },
50
51 #[error("decode failed: {detail}")]
53 DecodeFailed { detail: String },
54
55 #[error("end of stream")]
57 Eos,
58
59 #[error("source timeout")]
61 Timeout,
62
63 #[error("unsupported: {detail}")]
65 Unsupported { detail: String },
66
67 #[error("insecure RTSP rejected by RequireTls policy (use rtsps:// or set AllowInsecure)")]
69 InsecureRtspRejected,
70
71 #[error(
73 "custom pipeline rejected: set CustomPipelinePolicy::AllowTrusted on the runtime builder to enable custom pipelines"
74 )]
75 CustomPipelineRejected,
76}
77
78#[derive(Debug, Clone, thiserror::Error)]
83pub enum StageError {
84 #[error("stage `{stage_id}` processing failed: {detail}")]
86 ProcessingFailed { stage_id: StageId, detail: String },
87
88 #[error("stage `{stage_id}` resource exhausted")]
90 ResourceExhausted { stage_id: StageId },
91
92 #[error("stage `{stage_id}` model/dependency load failed: {detail}")]
94 ModelLoadFailed { stage_id: StageId, detail: String },
95}
96
97#[derive(Debug, thiserror::Error)]
99pub enum TemporalError {
100 #[error("track not found: {0}")]
102 TrackNotFound(crate::id::TrackId),
103
104 #[error("retention limit exceeded: {detail}")]
106 RetentionLimitExceeded { detail: String },
107}
108
109#[derive(Debug, thiserror::Error)]
111pub enum ViewError {
112 #[error("invalid motion report: {detail}")]
114 InvalidMotionReport { detail: String },
115
116 #[error("transform computation failed: {detail}")]
118 TransformFailed { detail: String },
119}
120
121#[derive(Debug, thiserror::Error)]
123pub enum RuntimeError {
124 #[error("feed not found: {feed_id}")]
126 FeedNotFound { feed_id: FeedId },
127
128 #[error("runtime is already running")]
130 AlreadyRunning,
131
132 #[error("feed is already paused")]
134 AlreadyPaused,
135
136 #[error("feed is not paused")]
138 NotPaused,
139
140 #[error("shutdown in progress")]
142 ShutdownInProgress,
143
144 #[error("feed limit exceeded (max: {max})")]
146 FeedLimitExceeded { max: usize },
147
148 #[error("internal registry lock poisoned")]
150 RegistryPoisoned,
151
152 #[error("thread spawn failed: {detail}")]
154 ThreadSpawnFailed { detail: String },
155}
156
157#[derive(Debug, thiserror::Error)]
159pub enum ConfigError {
160 #[error("invalid source: {detail}")]
162 InvalidSource { detail: String },
163
164 #[error("invalid policy: {detail}")]
166 InvalidPolicy { detail: String },
167
168 #[error("missing required field: `{field}`")]
170 MissingRequired { field: &'static str },
171
172 #[error("camera mode conflict: {detail}")]
176 CameraModeConflict { detail: String },
177
178 #[error("invalid capacity: {field} must be > 0")]
180 InvalidCapacity { field: &'static str },
181
182 #[error("stage validation failed: {detail}")]
184 StageValidation { detail: String },
185
186 #[error("duplicate batch processor id: {id}")]
188 DuplicateBatchProcessorId { id: StageId },
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn nv_error_from_media_error() {
197 let err: NvError = MediaError::Timeout.into();
198 assert!(matches!(err, NvError::Media(MediaError::Timeout)));
199 assert!(err.to_string().contains("timeout"));
200 }
201
202 #[test]
203 fn nv_error_from_config_error() {
204 let err: NvError = ConfigError::MissingRequired { field: "source" }.into();
205 assert!(matches!(
206 err,
207 NvError::Config(ConfigError::MissingRequired { .. })
208 ));
209 assert!(err.to_string().contains("source"));
210 }
211
212 #[test]
213 fn stage_error_includes_stage_id() {
214 let err = StageError::ProcessingFailed {
215 stage_id: StageId("detector"),
216 detail: "NaN output".into(),
217 };
218 let msg = err.to_string();
219 assert!(msg.contains("detector"));
220 assert!(msg.contains("NaN output"));
221 }
222
223 #[test]
224 fn runtime_error_display() {
225 let err = RuntimeError::FeedLimitExceeded { max: 64 };
226 assert!(err.to_string().contains("64"));
227 }
228
229 #[test]
230 fn media_error_is_clone() {
231 let err = MediaError::ConnectionFailed {
232 url: "rtsp://cam".into(),
233 detail: "timeout".into(),
234 };
235 let err2 = err.clone();
236 assert_eq!(err.to_string(), err2.to_string());
237 }
238}