1use std::path::Path;
2
3use orion_error::{ErrorReport, OperationContext, SourceFrame};
4
5pub mod key {
6 pub const CONFIG_KIND: &str = "config.kind";
7 pub const CONFIG_GROUP: &str = "config.group";
8 pub const CONFIG_SECTION: &str = "config.section";
9 pub const CONFIG_TYPE_NAME: &str = "config.type_name";
10 pub const CONFIG_LOADER: &str = "config.loader";
11
12 pub const FILE_PATH: &str = "file.path";
13 pub const DIR_PATH: &str = "dir.path";
14 pub const RESOURCE_PATH: &str = "resource.path";
15 pub const NETWORK_URL: &str = "network.url";
16
17 pub const RUNTIME_STAGE: &str = "runtime.stage";
18 pub const OPERATION_KIND: &str = "operation.kind";
19 pub const COMPONENT_KIND: &str = "component.kind";
20 pub const COMPONENT_NAME: &str = "component.name";
21
22 pub const HINT_CODE: &str = "hint.code";
23}
24
25pub trait MetaValue: Copy + Sized {
26 const KEY: &'static str;
27
28 fn as_str(self) -> &'static str;
29
30 fn parse(raw: &str) -> Option<Self>;
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ConfigKind {
35 Wpsrc,
36 SinkRoute,
37 SinkDefaults,
38 ConnectorDef,
39 Engine,
40 Wpgen,
41 Knowdb,
42 SourceRuntime,
43 SinkRuntime,
44}
45
46impl MetaValue for ConfigKind {
47 const KEY: &'static str = key::CONFIG_KIND;
48
49 fn as_str(self) -> &'static str {
50 match self {
51 Self::Wpsrc => "wpsrc",
52 Self::SinkRoute => "sink_route",
53 Self::SinkDefaults => "sink_defaults",
54 Self::ConnectorDef => "connector_def",
55 Self::Engine => "engine",
56 Self::Wpgen => "wpgen",
57 Self::Knowdb => "knowdb",
58 Self::SourceRuntime => "source_runtime",
59 Self::SinkRuntime => "sink_runtime",
60 }
61 }
62
63 fn parse(raw: &str) -> Option<Self> {
64 Some(match raw {
65 "wpsrc" => Self::Wpsrc,
66 "sink_route" => Self::SinkRoute,
67 "sink_defaults" => Self::SinkDefaults,
68 "connector_def" => Self::ConnectorDef,
69 "engine" => Self::Engine,
70 "wpgen" => Self::Wpgen,
71 "knowdb" => Self::Knowdb,
72 "source_runtime" => Self::SourceRuntime,
73 "sink_runtime" => Self::SinkRuntime,
74 _ => return None,
75 })
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum ConfigGroup {
81 Infra,
82 Business,
83}
84
85impl MetaValue for ConfigGroup {
86 const KEY: &'static str = key::CONFIG_GROUP;
87
88 fn as_str(self) -> &'static str {
89 match self {
90 Self::Infra => "infra",
91 Self::Business => "business",
92 }
93 }
94
95 fn parse(raw: &str) -> Option<Self> {
96 Some(match raw {
97 "infra" => Self::Infra,
98 "business" => Self::Business,
99 _ => return None,
100 })
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum RuntimeStage {
106 GeneratorGenerate,
107 SupervisorMonitor,
108 CollectorRecovery,
109 OrchestratorConfigLoad,
110 SystemOperations,
111}
112
113impl MetaValue for RuntimeStage {
114 const KEY: &'static str = key::RUNTIME_STAGE;
115
116 fn as_str(self) -> &'static str {
117 match self {
118 Self::GeneratorGenerate => "generator.generate",
119 Self::SupervisorMonitor => "supervisor.monitor",
120 Self::CollectorRecovery => "collector.recovery",
121 Self::OrchestratorConfigLoad => "orchestrator.config.load",
122 Self::SystemOperations => "system_operations",
123 }
124 }
125
126 fn parse(raw: &str) -> Option<Self> {
127 Some(match raw {
128 "generator.generate" => Self::GeneratorGenerate,
129 "supervisor.monitor" => Self::SupervisorMonitor,
130 "collector.recovery" => Self::CollectorRecovery,
131 "orchestrator.config.load" => Self::OrchestratorConfigLoad,
132 "system_operations" => Self::SystemOperations,
133 _ => return None,
134 })
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum OperationKind {
140 LoadConfigFile,
141 ParseConfig,
142 ValidateConfig,
143 ReadDir,
144 ReadDirEntry,
145 BuildSourceInstance,
146 BuildSinkInstance,
147 PluginValidate,
148 ReplayRescueFile,
149}
150
151impl MetaValue for OperationKind {
152 const KEY: &'static str = key::OPERATION_KIND;
153
154 fn as_str(self) -> &'static str {
155 match self {
156 Self::LoadConfigFile => "load_config_file",
157 Self::ParseConfig => "parse_config",
158 Self::ValidateConfig => "validate_config",
159 Self::ReadDir => "read_dir",
160 Self::ReadDirEntry => "read_dir_entry",
161 Self::BuildSourceInstance => "build_source_instance",
162 Self::BuildSinkInstance => "build_sink_instance",
163 Self::PluginValidate => "plugin_validate",
164 Self::ReplayRescueFile => "replay_rescue_file",
165 }
166 }
167
168 fn parse(raw: &str) -> Option<Self> {
169 Some(match raw {
170 "load_config_file" => Self::LoadConfigFile,
171 "parse_config" => Self::ParseConfig,
172 "validate_config" => Self::ValidateConfig,
173 "read_dir" => Self::ReadDir,
174 "read_dir_entry" => Self::ReadDirEntry,
175 "build_source_instance" => Self::BuildSourceInstance,
176 "build_sink_instance" => Self::BuildSinkInstance,
177 "plugin_validate" => Self::PluginValidate,
178 "replay_rescue_file" => Self::ReplayRescueFile,
179 _ => return None,
180 })
181 }
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum ComponentKind {
186 Source,
187 Sink,
188 Connector,
189 Generator,
190 Monitor,
191 Rescue,
192 Checkpoint,
193}
194
195impl MetaValue for ComponentKind {
196 const KEY: &'static str = key::COMPONENT_KIND;
197
198 fn as_str(self) -> &'static str {
199 match self {
200 Self::Source => "source",
201 Self::Sink => "sink",
202 Self::Connector => "connector",
203 Self::Generator => "generator",
204 Self::Monitor => "monitor",
205 Self::Rescue => "rescue",
206 Self::Checkpoint => "checkpoint",
207 }
208 }
209
210 fn parse(raw: &str) -> Option<Self> {
211 Some(match raw {
212 "source" => Self::Source,
213 "sink" => Self::Sink,
214 "connector" => Self::Connector,
215 "generator" => Self::Generator,
216 "monitor" => Self::Monitor,
217 "rescue" => Self::Rescue,
218 "checkpoint" => Self::Checkpoint,
219 _ => return None,
220 })
221 }
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225pub enum HintCode {
226 WpsrcTomlSchema,
227 SinkRouteTomlSchema,
228 SinkDefaultsTomlSchema,
229 KafkaFeatureRequired,
230 DuplicateSourceKey,
231 InvalidSyslogProtocol,
232}
233
234impl MetaValue for HintCode {
235 const KEY: &'static str = key::HINT_CODE;
236
237 fn as_str(self) -> &'static str {
238 match self {
239 Self::WpsrcTomlSchema => "wpsrc_toml_schema",
240 Self::SinkRouteTomlSchema => "sink_route_toml_schema",
241 Self::SinkDefaultsTomlSchema => "sink_defaults_toml_schema",
242 Self::KafkaFeatureRequired => "kafka_feature_required",
243 Self::DuplicateSourceKey => "duplicate_source_key",
244 Self::InvalidSyslogProtocol => "invalid_syslog_protocol",
245 }
246 }
247
248 fn parse(raw: &str) -> Option<Self> {
249 Some(match raw {
250 "wpsrc_toml_schema" => Self::WpsrcTomlSchema,
251 "sink_route_toml_schema" => Self::SinkRouteTomlSchema,
252 "sink_defaults_toml_schema" => Self::SinkDefaultsTomlSchema,
253 "kafka_feature_required" => Self::KafkaFeatureRequired,
254 "duplicate_source_key" => Self::DuplicateSourceKey,
255 "invalid_syslog_protocol" => Self::InvalidSyslogProtocol,
256 _ => return None,
257 })
258 }
259}
260
261pub trait OperationContextMetaExt: Sized {
262 fn with_meta_value<M: MetaValue>(self, value: M) -> Self;
263
264 fn with_file_path(self, path: &Path) -> Self;
265
266 fn with_dir_path(self, path: &Path) -> Self;
267
268 fn with_resource_path(self, path: &Path) -> Self;
269
270 fn with_network_url(self, url: impl Into<String>) -> Self;
271
272 fn with_component_name(self, name: impl Into<String>) -> Self;
273
274 fn with_config_type_name(self, name: impl Into<String>) -> Self;
275
276 fn with_config_section(self, section: impl Into<String>) -> Self;
277}
278
279impl OperationContextMetaExt for OperationContext {
280 fn with_meta_value<M: MetaValue>(mut self, value: M) -> Self {
281 self.record_meta(M::KEY, value.as_str());
282 self
283 }
284
285 fn with_file_path(mut self, path: &Path) -> Self {
286 self.record_meta(key::FILE_PATH, path.display().to_string());
287 self
288 }
289
290 fn with_dir_path(mut self, path: &Path) -> Self {
291 self.record_meta(key::DIR_PATH, path.display().to_string());
292 self
293 }
294
295 fn with_resource_path(mut self, path: &Path) -> Self {
296 self.record_meta(key::RESOURCE_PATH, path.display().to_string());
297 self
298 }
299
300 fn with_network_url(mut self, url: impl Into<String>) -> Self {
301 self.record_meta(key::NETWORK_URL, url.into());
302 self
303 }
304
305 fn with_component_name(mut self, name: impl Into<String>) -> Self {
306 self.record_meta(key::COMPONENT_NAME, name.into());
307 self
308 }
309
310 fn with_config_type_name(mut self, name: impl Into<String>) -> Self {
311 self.record_meta(key::CONFIG_TYPE_NAME, name.into());
312 self
313 }
314
315 fn with_config_section(mut self, section: impl Into<String>) -> Self {
316 self.record_meta(key::CONFIG_SECTION, section.into());
317 self
318 }
319}
320
321pub fn first_meta_str<'a>(report: &'a ErrorReport, key: &str) -> Option<&'a str> {
322 report.root_metadata.get_str(key).or_else(|| {
323 report
324 .source_frames
325 .iter()
326 .find_map(|frame| frame.metadata.get_str(key))
327 })
328}
329
330pub fn first_meta_enum<M: MetaValue>(report: &ErrorReport) -> Option<M> {
331 first_meta_str(report, M::KEY).and_then(M::parse)
332}
333
334pub fn frame_meta_enum<M: MetaValue>(frame: &SourceFrame) -> Option<M> {
335 frame.metadata.get_str(M::KEY).and_then(M::parse)
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use orion_error::ErrorMetadata;
342
343 #[test]
344 fn config_kind_round_trip() {
345 assert_eq!(
346 ConfigKind::parse(ConfigKind::Wpsrc.as_str()),
347 Some(ConfigKind::Wpsrc)
348 );
349 assert_eq!(
350 ConfigKind::parse(ConfigKind::SinkDefaults.as_str()),
351 Some(ConfigKind::SinkDefaults)
352 );
353 }
354
355 #[test]
356 fn operation_context_meta_ext_records_typed_and_path_metadata() {
357 let ctx = OperationContext::want("load")
358 .with_meta_value(ConfigKind::Wpsrc)
359 .with_meta_value(OperationKind::LoadConfigFile)
360 .with_file_path(Path::new("/tmp/wpsrc.toml"))
361 .with_component_name("file_1");
362
363 assert_eq!(ctx.metadata().get_str(key::CONFIG_KIND), Some("wpsrc"));
364 assert_eq!(
365 ctx.metadata().get_str(key::OPERATION_KIND),
366 Some("load_config_file")
367 );
368 assert_eq!(
369 ctx.metadata().get_str(key::FILE_PATH),
370 Some("/tmp/wpsrc.toml")
371 );
372 assert_eq!(ctx.metadata().get_str(key::COMPONENT_NAME), Some("file_1"));
373 }
374
375 #[test]
376 fn first_meta_enum_prefers_root_metadata() {
377 let mut root = ErrorMetadata::new();
378 root.insert(key::CONFIG_KIND, ConfigKind::SinkRoute.as_str());
379
380 let report = ErrorReport {
381 reason: "configuration error".to_string(),
382 detail: None,
383 position: None,
384 want: None,
385 path: None,
386 context: Vec::new(),
387 root_metadata: root,
388 source_frames: Vec::new(),
389 };
390
391 assert_eq!(
392 first_meta_enum::<ConfigKind>(&report),
393 Some(ConfigKind::SinkRoute)
394 );
395 }
396
397 #[test]
398 fn frame_meta_enum_reads_typed_metadata() {
399 let mut metadata = ErrorMetadata::new();
400 metadata.insert(key::CONFIG_GROUP, ConfigGroup::Infra.as_str());
401
402 let frame = SourceFrame {
403 index: 0,
404 message: "configuration error".to_string(),
405 display: None,
406 debug: String::new(),
407 type_name: None,
408 error_code: None,
409 reason: None,
410 want: None,
411 path: None,
412 detail: None,
413 metadata,
414 is_root_cause: true,
415 };
416
417 assert_eq!(
418 frame_meta_enum::<ConfigGroup>(&frame),
419 Some(ConfigGroup::Infra)
420 );
421 }
422}