Skip to main content

platform/config/
registry.rs

1//! 配置注册表
2//!
3//! 静态注册表,声明每个可配置字段的元信息(类型、默认值、是否支持动态覆盖/热重载)。
4
5use serde::Serialize;
6
7/// 配置值类型
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
9#[serde(rename_all = "lowercase")]
10pub enum ConfigValueType {
11    String,
12    U8,
13    U16,
14    U32,
15    U64,
16    Bool,
17    Enum,
18    /// "start-end" where both are u16, start <= end
19    Range16,
20    /// IP address (v4 or v6), validated via std::net::IpAddr
21    Ip,
22    /// Local filesystem path
23    Fpath,
24    /// Domain name / hostname
25    Domain,
26    /// URI path (starts with `/`)
27    #[serde(rename = "uri_path")]
28    UriPath,
29}
30
31impl ConfigValueType {
32    /// Validate that a string value can be parsed as this type.
33    /// For Enum validation, use `ConfigFieldDef::validate` which checks choices.
34    pub fn validate_str(&self, value: &str) -> bool {
35        match self {
36            ConfigValueType::String | ConfigValueType::Enum | ConfigValueType::Fpath => true,
37            ConfigValueType::U8 => value.parse::<u8>().is_ok(),
38            ConfigValueType::U16 => value.parse::<u16>().is_ok(),
39            ConfigValueType::U32 => value.parse::<u32>().is_ok(),
40            ConfigValueType::U64 => value.parse::<u64>().is_ok(),
41            ConfigValueType::Bool => value == "true" || value == "false",
42            ConfigValueType::Range16 => {
43                let Some((a, b)) = value.split_once('-') else {
44                    return false;
45                };
46                let (Ok(start), Ok(end)) = (a.parse::<u16>(), b.parse::<u16>()) else {
47                    return false;
48                };
49                start <= end
50            }
51            ConfigValueType::Ip => value.parse::<std::net::IpAddr>().is_ok(),
52            ConfigValueType::Domain => {
53                !value.is_empty()
54                    && value.len() <= 253
55                    && value.split('.').all(|label| {
56                        !label.is_empty()
57                            && label.len() <= 63
58                            && label
59                                .bytes()
60                                .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
61                    })
62            }
63            ConfigValueType::UriPath => value.starts_with('/'),
64        }
65    }
66}
67
68impl std::fmt::Display for ConfigValueType {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        match self {
71            ConfigValueType::String => write!(f, "string"),
72            ConfigValueType::U8 => write!(f, "u8"),
73            ConfigValueType::U16 => write!(f, "u16"),
74            ConfigValueType::U32 => write!(f, "u32"),
75            ConfigValueType::U64 => write!(f, "u64"),
76            ConfigValueType::Bool => write!(f, "bool"),
77            ConfigValueType::Enum => write!(f, "enum"),
78            ConfigValueType::Range16 => write!(f, "range16"),
79            ConfigValueType::Ip => write!(f, "ip"),
80            ConfigValueType::Fpath => write!(f, "fpath"),
81            ConfigValueType::Domain => write!(f, "domain"),
82            ConfigValueType::UriPath => write!(f, "uri_path"),
83        }
84    }
85}
86
87/// 配置字段定义
88#[derive(Debug, Clone, Serialize)]
89pub struct ConfigFieldDef {
90    /// Dot-separated key, e.g. "turn.realm"
91    pub key: &'static str,
92    /// TOML navigation path, e.g. "turn.realm"
93    pub toml_path: &'static str,
94    /// Value type
95    pub value_type: ConfigValueType,
96    /// Default value (as string)
97    pub default_value: &'static str,
98    /// Can be overridden via API at runtime
99    pub dynamic: bool,
100    /// Takes effect on SIGHUP reload (without restart)
101    pub reloadable: bool,
102    /// Human-readable description
103    pub description: &'static str,
104    /// Service this field belongs to
105    pub service: &'static str,
106    /// Valid choices for Enum type (empty for other types)
107    pub choices: &'static [&'static str],
108}
109
110impl ConfigFieldDef {
111    /// Validate a string value against this field's type and choices.
112    pub fn validate(&self, value: &str) -> bool {
113        if !self.value_type.validate_str(value) {
114            return false;
115        }
116        if self.value_type == ConfigValueType::Enum && !self.choices.is_empty() {
117            return self.choices.contains(&value);
118        }
119        true
120    }
121}
122
123/// Complete static registry of all config fields.
124static REGISTRY: &[ConfigFieldDef] = &[
125    // ── Platform ──────────────────────────────────────────────────
126    ConfigFieldDef {
127        key: "enable",
128        toml_path: "enable",
129        value_type: ConfigValueType::U8,
130        default_value: "31",
131        dynamic: false,
132        reloadable: false,
133        description: "Service enable bitmask",
134        service: "platform",
135        choices: &[],
136    },
137    ConfigFieldDef {
138        key: "name",
139        toml_path: "name",
140        value_type: ConfigValueType::String,
141        default_value: "actrix-default",
142        dynamic: false,
143        reloadable: false,
144        description: "Server instance name",
145        service: "platform",
146        choices: &[],
147    },
148    ConfigFieldDef {
149        key: "env",
150        toml_path: "env",
151        value_type: ConfigValueType::Enum,
152        default_value: "dev",
153        dynamic: false,
154        reloadable: false,
155        description: "Environment",
156        service: "platform",
157        choices: &["dev", "prod", "test"],
158    },
159    ConfigFieldDef {
160        key: "location_tag",
161        toml_path: "location_tag",
162        value_type: ConfigValueType::String,
163        default_value: "default-location",
164        dynamic: false,
165        reloadable: false,
166        description: "Geographic location tag",
167        service: "platform",
168        choices: &[],
169    },
170    ConfigFieldDef {
171        key: "sqlite_path",
172        toml_path: "sqlite_path",
173        value_type: ConfigValueType::Fpath,
174        default_value: "database",
175        dynamic: false,
176        reloadable: false,
177        description: "SQLite database directory",
178        service: "platform",
179        choices: &[],
180    },
181    ConfigFieldDef {
182        key: "bind.http.ip",
183        toml_path: "bind.http.ip",
184        value_type: ConfigValueType::Ip,
185        default_value: "::",
186        dynamic: false,
187        reloadable: false,
188        description: "HTTP listen address",
189        service: "platform",
190        choices: &[],
191    },
192    ConfigFieldDef {
193        key: "bind.http.port",
194        toml_path: "bind.http.port",
195        value_type: ConfigValueType::U16,
196        default_value: "8080",
197        dynamic: false,
198        reloadable: false,
199        description: "HTTP port",
200        service: "platform",
201        choices: &[],
202    },
203    ConfigFieldDef {
204        key: "bind.http.domain_name",
205        toml_path: "bind.http.domain_name",
206        value_type: ConfigValueType::Domain,
207        default_value: "localhost",
208        dynamic: false,
209        reloadable: false,
210        description: "HTTP domain name",
211        service: "platform",
212        choices: &[],
213    },
214    ConfigFieldDef {
215        key: "bind.http.advertised_ip",
216        toml_path: "bind.http.advertised_ip",
217        value_type: ConfigValueType::Ip,
218        default_value: "127.0.0.1",
219        dynamic: false,
220        reloadable: false,
221        description: "HTTP advertised IP",
222        service: "platform",
223        choices: &[],
224    },
225    ConfigFieldDef {
226        key: "bind.http.advertised_port",
227        toml_path: "bind.http.advertised_port",
228        value_type: ConfigValueType::U16,
229        default_value: "0",
230        dynamic: false,
231        reloadable: false,
232        description: "HTTP advertised port (0 = same as port)",
233        service: "platform",
234        choices: &[],
235    },
236    ConfigFieldDef {
237        key: "bind.http.cert",
238        toml_path: "bind.http.cert",
239        value_type: ConfigValueType::Fpath,
240        default_value: "",
241        dynamic: false,
242        reloadable: false,
243        description: "TLS certificate path (enables HTTPS)",
244        service: "platform",
245        choices: &[],
246    },
247    ConfigFieldDef {
248        key: "bind.http.key",
249        toml_path: "bind.http.key",
250        value_type: ConfigValueType::Fpath,
251        default_value: "",
252        dynamic: false,
253        reloadable: false,
254        description: "TLS private key path (enables HTTPS)",
255        service: "platform",
256        choices: &[],
257    },
258    ConfigFieldDef {
259        key: "recording.sink",
260        toml_path: "recording.sink",
261        value_type: ConfigValueType::String,
262        default_value: "",
263        dynamic: false,
264        reloadable: false,
265        description: "Global sink URI (file:// | otlp+http:// | otlp+grpc://)",
266        service: "platform",
267        choices: &[],
268    },
269    ConfigFieldDef {
270        key: "recording.service_name",
271        toml_path: "recording.service_name",
272        value_type: ConfigValueType::String,
273        default_value: "actrix",
274        dynamic: false,
275        reloadable: false,
276        description: "OTLP service name",
277        service: "platform",
278        choices: &[],
279    },
280    // ── observability channel ──
281    ConfigFieldDef {
282        key: "recording.observability.filter",
283        toml_path: "recording.observability.filter",
284        value_type: ConfigValueType::Enum,
285        default_value: "digest",
286        dynamic: true,
287        reloadable: true,
288        description: "Observability resolution: off / digest / detailed / full",
289        service: "platform",
290        choices: &["off", "digest", "detailed", "full"],
291    },
292    ConfigFieldDef {
293        key: "recording.observability.sink",
294        toml_path: "recording.observability.sink",
295        value_type: ConfigValueType::String,
296        default_value: "",
297        dynamic: false,
298        reloadable: false,
299        description: "Observability channel sink override",
300        service: "platform",
301        choices: &[],
302    },
303    // ── audit channel ──
304    ConfigFieldDef {
305        key: "recording.audit.filter",
306        toml_path: "recording.audit.filter",
307        value_type: ConfigValueType::Enum,
308        default_value: "mutations",
309        dynamic: true,
310        reloadable: true,
311        description: "Audit scope: off / mutations / all",
312        service: "platform",
313        choices: &["off", "mutations", "all"],
314    },
315    ConfigFieldDef {
316        key: "recording.audit.sink",
317        toml_path: "recording.audit.sink",
318        value_type: ConfigValueType::String,
319        default_value: "",
320        dynamic: false,
321        reloadable: false,
322        description: "Audit channel sink override",
323        service: "platform",
324        choices: &[],
325    },
326    // ── security channel ──
327    ConfigFieldDef {
328        key: "recording.security.filter",
329        toml_path: "recording.security.filter",
330        value_type: ConfigValueType::Enum,
331        default_value: "all",
332        dynamic: true,
333        reloadable: true,
334        description: "Security severity threshold: off / critical / high / medium / all",
335        service: "platform",
336        choices: &["off", "critical", "high", "medium", "all"],
337    },
338    ConfigFieldDef {
339        key: "recording.security.sink",
340        toml_path: "recording.security.sink",
341        value_type: ConfigValueType::String,
342        default_value: "",
343        dynamic: false,
344        reloadable: false,
345        description: "Security channel sink override",
346        service: "platform",
347        choices: &[],
348    },
349    // ── operations channel ──
350    ConfigFieldDef {
351        key: "recording.operations.filter",
352        toml_path: "recording.operations.filter",
353        value_type: ConfigValueType::Enum,
354        default_value: "lifecycle",
355        dynamic: true,
356        reloadable: true,
357        description: "Operations detail: off / lifecycle / detailed",
358        service: "platform",
359        choices: &["off", "lifecycle", "detailed"],
360    },
361    ConfigFieldDef {
362        key: "recording.operations.sink",
363        toml_path: "recording.operations.sink",
364        value_type: ConfigValueType::String,
365        default_value: "",
366        dynamic: false,
367        reloadable: false,
368        description: "Operations channel sink override",
369        service: "platform",
370        choices: &[],
371    },
372    ConfigFieldDef {
373        key: "control.head",
374        toml_path: "control.head",
375        value_type: ConfigValueType::Enum,
376        default_value: "admin_ui",
377        dynamic: false,
378        reloadable: false,
379        description: "Legacy control plane mode override (prefer admin_ui.enabled / grpc_api.enabled)",
380        service: "platform",
381        choices: &["admin_ui", "grpc_api"],
382    },
383    ConfigFieldDef {
384        key: "control.admin_ui.enabled",
385        toml_path: "control.admin_ui.enabled",
386        value_type: ConfigValueType::Bool,
387        default_value: "true",
388        dynamic: false,
389        reloadable: false,
390        description: "Enable local Admin UI",
391        service: "platform",
392        choices: &[],
393    },
394    ConfigFieldDef {
395        key: "control.grpc_api.enabled",
396        toml_path: "control.grpc_api.enabled",
397        value_type: ConfigValueType::Bool,
398        default_value: "false",
399        dynamic: false,
400        reloadable: false,
401        description: "Enable NodeAdminService gRPC control API",
402        service: "platform",
403        choices: &[],
404    },
405    ConfigFieldDef {
406        key: "control.admin_ui.session_expiry_secs",
407        toml_path: "control.admin_ui.session_expiry_secs",
408        value_type: ConfigValueType::U64,
409        default_value: "86400",
410        dynamic: true,
411        reloadable: true,
412        description: "Admin session TTL",
413        service: "platform",
414        choices: &[],
415    },
416    // ── ICE binding (shared by STUN + TURN) ────────────────────────
417    ConfigFieldDef {
418        key: "bind.ice.ip",
419        toml_path: "bind.ice.ip",
420        value_type: ConfigValueType::Ip,
421        default_value: "0.0.0.0",
422        dynamic: false,
423        reloadable: false,
424        description: "Local IP address the ICE server listens on",
425        service: "platform",
426        choices: &[],
427    },
428    ConfigFieldDef {
429        key: "bind.ice.port",
430        toml_path: "bind.ice.port",
431        value_type: ConfigValueType::U16,
432        default_value: "3478",
433        dynamic: false,
434        reloadable: false,
435        description: "UDP port the ICE server binds to",
436        service: "platform",
437        choices: &[],
438    },
439    ConfigFieldDef {
440        key: "bind.ice.advertised_ip",
441        toml_path: "bind.ice.advertised_ip",
442        value_type: ConfigValueType::Ip,
443        default_value: "127.0.0.1",
444        dynamic: false,
445        reloadable: false,
446        description: "Public IP advertised to clients",
447        service: "platform",
448        choices: &[],
449    },
450    ConfigFieldDef {
451        key: "bind.ice.advertised_port",
452        toml_path: "bind.ice.advertised_port",
453        value_type: ConfigValueType::U16,
454        default_value: "3478",
455        dynamic: false,
456        reloadable: false,
457        description: "Public-facing port advertised to clients",
458        service: "platform",
459        choices: &[],
460    },
461    ConfigFieldDef {
462        key: "turn.relay_port_range",
463        toml_path: "turn.relay_port_range",
464        value_type: ConfigValueType::Range16,
465        default_value: "49152-65535",
466        dynamic: false,
467        reloadable: false,
468        description: "UDP port range for relay allocations",
469        service: "platform",
470        choices: &[],
471    },
472    ConfigFieldDef {
473        key: "turn.realm",
474        toml_path: "turn.realm",
475        value_type: ConfigValueType::Domain,
476        default_value: "actrix.local",
477        dynamic: true,
478        reloadable: true,
479        description: "Credential scope for long-term authentication (RFC 8489)",
480        service: "turn",
481        choices: &[],
482    },
483    // ── Signaling ─────────────────────────────────────────────────
484    ConfigFieldDef {
485        key: "services.signaling.server.ws_path",
486        toml_path: "services.signaling.server.ws_path",
487        value_type: ConfigValueType::UriPath,
488        default_value: "/signaling",
489        dynamic: false,
490        reloadable: true,
491        description: "WebSocket endpoint path for client connections",
492        service: "signaling",
493        choices: &[],
494    },
495    ConfigFieldDef {
496        key: "services.signaling.server.rate_limit.connection.enabled",
497        toml_path: "services.signaling.server.rate_limit.connection.enabled",
498        value_type: ConfigValueType::Bool,
499        default_value: "true",
500        dynamic: true,
501        reloadable: true,
502        description: "Enable connection-level rate limiting",
503        service: "signaling",
504        choices: &[],
505    },
506    ConfigFieldDef {
507        key: "services.signaling.server.rate_limit.connection.per_minute",
508        toml_path: "services.signaling.server.rate_limit.connection.per_minute",
509        value_type: ConfigValueType::U32,
510        default_value: "5",
511        dynamic: true,
512        reloadable: true,
513        description: "Max new connections per IP per minute",
514        service: "signaling",
515        choices: &[],
516    },
517    ConfigFieldDef {
518        key: "services.signaling.server.rate_limit.connection.burst_size",
519        toml_path: "services.signaling.server.rate_limit.connection.burst_size",
520        value_type: ConfigValueType::U32,
521        default_value: "10",
522        dynamic: true,
523        reloadable: true,
524        description: "Burst allowance for connection rate",
525        service: "signaling",
526        choices: &[],
527    },
528    ConfigFieldDef {
529        key: "services.signaling.server.rate_limit.connection.max_concurrent_per_ip",
530        toml_path: "services.signaling.server.rate_limit.connection.max_concurrent_per_ip",
531        value_type: ConfigValueType::U32,
532        default_value: "100",
533        dynamic: true,
534        reloadable: true,
535        description: "Max concurrent connections per IP",
536        service: "signaling",
537        choices: &[],
538    },
539    ConfigFieldDef {
540        key: "services.signaling.server.rate_limit.message.enabled",
541        toml_path: "services.signaling.server.rate_limit.message.enabled",
542        value_type: ConfigValueType::Bool,
543        default_value: "true",
544        dynamic: true,
545        reloadable: true,
546        description: "Enable message-level rate limiting",
547        service: "signaling",
548        choices: &[],
549    },
550    ConfigFieldDef {
551        key: "services.signaling.server.rate_limit.message.per_second",
552        toml_path: "services.signaling.server.rate_limit.message.per_second",
553        value_type: ConfigValueType::U32,
554        default_value: "10",
555        dynamic: true,
556        reloadable: true,
557        description: "Max messages per connection per second",
558        service: "signaling",
559        choices: &[],
560    },
561    ConfigFieldDef {
562        key: "services.signaling.server.rate_limit.message.burst_size",
563        toml_path: "services.signaling.server.rate_limit.message.burst_size",
564        value_type: ConfigValueType::U32,
565        default_value: "50",
566        dynamic: true,
567        reloadable: true,
568        description: "Burst allowance for message rate",
569        service: "signaling",
570        choices: &[],
571    },
572    // ── AIS ───────────────────────────────────────────────────────
573    ConfigFieldDef {
574        key: "services.ais.server.token_ttl_secs",
575        toml_path: "services.ais.server.token_ttl_secs",
576        value_type: ConfigValueType::U64,
577        default_value: "3600",
578        dynamic: true,
579        reloadable: true,
580        description: "Token time-to-live in seconds",
581        service: "ais",
582        choices: &[],
583    },
584    ConfigFieldDef {
585        key: "services.ais.server.signaling_heartbeat_interval_secs",
586        toml_path: "services.ais.server.signaling_heartbeat_interval_secs",
587        value_type: ConfigValueType::U32,
588        default_value: "30",
589        dynamic: true,
590        reloadable: true,
591        description: "Heartbeat interval for signaling connections",
592        service: "ais",
593        choices: &[],
594    },
595    // ── Signer ────────────────────────────────────────────────────────
596    ConfigFieldDef {
597        key: "services.signer.storage.key_ttl_seconds",
598        toml_path: "services.signer.storage.key_ttl_seconds",
599        value_type: ConfigValueType::U64,
600        default_value: "3600",
601        dynamic: true,
602        reloadable: true,
603        description: "Key time-to-live in seconds",
604        service: "signer",
605        choices: &[],
606    },
607    ConfigFieldDef {
608        key: "services.signer.tolerance_seconds",
609        toml_path: "services.signer.tolerance_seconds",
610        value_type: ConfigValueType::U64,
611        default_value: "300",
612        dynamic: true,
613        reloadable: true,
614        description: "Clock skew tolerance for key validation",
615        service: "signer",
616        choices: &[],
617    },
618    // ── Monitoring ─────────────────────────────────────────────────
619    ConfigFieldDef {
620        key: "monitoring.htpasswd_file",
621        toml_path: "monitoring.htpasswd_file",
622        value_type: ConfigValueType::Fpath,
623        default_value: "",
624        dynamic: true,
625        reloadable: true,
626        description: "Path to htpasswd file for monitoring endpoint Basic Auth",
627        service: "platform",
628        choices: &[],
629    },
630];
631
632/// Look up a field definition by its key.
633pub fn get_field(key: &str) -> Option<&'static ConfigFieldDef> {
634    REGISTRY.iter().find(|f| f.key == key)
635}
636
637/// Get all field definitions for a given service.
638pub fn fields_for_service(service: &str) -> Vec<&'static ConfigFieldDef> {
639    REGISTRY.iter().filter(|f| f.service == service).collect()
640}
641
642/// Get all field definitions in the registry.
643pub fn all_fields() -> &'static [ConfigFieldDef] {
644    REGISTRY
645}
646
647#[cfg(test)]
648mod tests {
649    use super::*;
650
651    #[test]
652    fn test_get_field() {
653        let field = get_field("turn.realm").unwrap();
654        assert_eq!(field.service, "turn");
655        assert!(field.dynamic);
656        assert!(field.reloadable);
657        assert_eq!(field.default_value, "actrix.local");
658    }
659
660    #[test]
661    fn test_get_field_not_found() {
662        assert!(get_field("nonexistent.field").is_none());
663    }
664
665    #[test]
666    fn test_fields_for_service() {
667        let platform_fields = fields_for_service("platform");
668        assert_eq!(platform_fields.len(), 32);
669        assert!(platform_fields.iter().all(|f| f.service == "platform"));
670
671        let stun_fields = fields_for_service("stun");
672        assert_eq!(stun_fields.len(), 0);
673
674        let signaling_fields = fields_for_service("signaling");
675        assert_eq!(signaling_fields.len(), 8);
676    }
677
678    #[test]
679    fn test_validate_type() {
680        assert!(ConfigValueType::U8.validate_str("31"));
681        assert!(!ConfigValueType::U8.validate_str("256"));
682        assert!(ConfigValueType::U16.validate_str("3478"));
683        assert!(!ConfigValueType::U16.validate_str("99999"));
684        assert!(ConfigValueType::U64.validate_str("99999"));
685        assert!(ConfigValueType::Bool.validate_str("true"));
686        assert!(!ConfigValueType::Bool.validate_str("yes"));
687        assert!(ConfigValueType::String.validate_str("anything"));
688
689        assert!(ConfigValueType::U32.validate_str("100000"));
690        assert!(!ConfigValueType::U32.validate_str("-1"));
691
692        // Ip
693        assert!(ConfigValueType::Ip.validate_str("127.0.0.1"));
694        assert!(ConfigValueType::Ip.validate_str("0.0.0.0"));
695        assert!(ConfigValueType::Ip.validate_str("::"));
696        assert!(ConfigValueType::Ip.validate_str("::1"));
697        assert!(ConfigValueType::Ip.validate_str("2001:db8::1"));
698        assert!(!ConfigValueType::Ip.validate_str("not-an-ip"));
699        assert!(!ConfigValueType::Ip.validate_str("999.999.999.999"));
700
701        // Fpath
702        assert!(ConfigValueType::Fpath.validate_str("/etc/actrix/config.toml"));
703        assert!(ConfigValueType::Fpath.validate_str("relative/path"));
704
705        // Domain
706        assert!(ConfigValueType::Domain.validate_str("localhost"));
707        assert!(ConfigValueType::Domain.validate_str("actrix.example.com"));
708        assert!(ConfigValueType::Domain.validate_str("actrix.local"));
709        assert!(!ConfigValueType::Domain.validate_str(""));
710        assert!(!ConfigValueType::Domain.validate_str("bad domain.com")); // space
711
712        // UriPath
713        assert!(ConfigValueType::UriPath.validate_str("/signaling"));
714        assert!(ConfigValueType::UriPath.validate_str("/"));
715        assert!(!ConfigValueType::UriPath.validate_str("signaling")); // no leading slash
716
717        // Range16
718        assert!(ConfigValueType::Range16.validate_str("49152-65535"));
719        assert!(ConfigValueType::Range16.validate_str("1024-1024"));
720        assert!(!ConfigValueType::Range16.validate_str("65535-1024")); // start > end
721        assert!(!ConfigValueType::Range16.validate_str("abc-def"));
722        assert!(!ConfigValueType::Range16.validate_str("1024")); // no dash
723    }
724
725    #[test]
726    fn test_validate_enum() {
727        let field = get_field("env").unwrap();
728        assert_eq!(field.value_type, ConfigValueType::Enum);
729        assert!(field.validate("dev"));
730        assert!(field.validate("prod"));
731        assert!(!field.validate("staging"));
732
733        let level = get_field("recording.observability.filter").unwrap();
734        assert!(level.validate("digest"));
735        assert!(level.validate("full"));
736        assert!(!level.validate("verbose"));
737    }
738
739    #[test]
740    fn test_all_fields_have_valid_defaults() {
741        for field in all_fields() {
742            assert!(
743                field.validate(field.default_value),
744                "Field '{}' has invalid default '{}' for type {:?}",
745                field.key,
746                field.default_value,
747                field.value_type
748            );
749        }
750    }
751}