1use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::{Arc, RwLock};
10
11use async_trait::async_trait;
12use chrono::{DateTime, Duration, Utc};
13use dashmap::DashMap;
14use serde::{Deserialize, Serialize};
15use tracing::info;
16
17use crate::error::KernelError;
18use crate::health::HealthStatus;
19use crate::process::Pid;
20use crate::service::{ServiceType, SystemService};
21
22#[cfg(feature = "exochain")]
23use crate::chain::ChainManager;
24#[cfg(feature = "exochain")]
25use crate::gate::GateBackend;
26
27#[non_exhaustive]
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34pub enum ConfigValue {
35 Text(String),
37 Integer(i64),
39 Float(f64),
41 Boolean(bool),
43 Json(serde_json::Value),
45}
46
47impl ConfigValue {
48 pub fn to_json(&self) -> serde_json::Value {
50 match self {
51 ConfigValue::Text(s) => serde_json::Value::String(s.clone()),
52 ConfigValue::Integer(n) => serde_json::json!(n),
53 ConfigValue::Float(f) => serde_json::json!(f),
54 ConfigValue::Boolean(b) => serde_json::json!(b),
55 ConfigValue::Json(v) => v.clone(),
56 }
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct ConfigEntry {
67 pub key: String,
69 pub namespace: String,
71 pub value: ConfigValue,
73 pub updated_at: DateTime<Utc>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct ConfigChange {
84 pub namespace: String,
86 pub key: String,
88 pub old_value: Option<serde_json::Value>,
90 pub new_value: Option<serde_json::Value>,
92 pub changed_by: Pid,
94 pub timestamp: DateTime<Utc>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct SecretRef {
105 pub namespace: String,
107 pub key: String,
109 pub expires_at: DateTime<Utc>,
111 pub scoped_to: Vec<Pid>,
113}
114
115pub struct ConfigService {
125 configs: DashMap<String, serde_json::Value>,
127 entries: DashMap<String, ConfigEntry>,
129 secrets: DashMap<String, Vec<u8>>,
131 secret_refs: DashMap<String, SecretRef>,
133 subscribers: DashMap<String, Vec<Arc<RwLock<Vec<ConfigChange>>>>>,
135 encryption_key: [u8; 32],
137 change_log: RwLock<Vec<ConfigChange>>,
139 set_count: AtomicU64,
141 #[cfg(feature = "exochain")]
143 chain_manager: Option<Arc<ChainManager>>,
144 #[cfg(feature = "exochain")]
146 governance_gate: Option<Arc<dyn GateBackend>>,
147}
148
149impl ConfigService {
150 pub fn new(encryption_key: [u8; 32]) -> Self {
152 Self {
153 configs: DashMap::new(),
154 entries: DashMap::new(),
155 secrets: DashMap::new(),
156 secret_refs: DashMap::new(),
157 subscribers: DashMap::new(),
158 encryption_key,
159 change_log: RwLock::new(Vec::new()),
160 set_count: AtomicU64::new(0),
161 #[cfg(feature = "exochain")]
162 chain_manager: None,
163 #[cfg(feature = "exochain")]
164 governance_gate: None,
165 }
166 }
167
168 pub fn new_default() -> Self {
170 Self::new([0u8; 32])
171 }
172
173 #[cfg(feature = "exochain")]
175 pub fn with_chain_manager(mut self, cm: Arc<ChainManager>) -> Self {
176 self.chain_manager = Some(cm);
177 self
178 }
179
180 #[cfg(feature = "exochain")]
182 pub fn with_governance_gate(mut self, gate: Arc<dyn GateBackend>) -> Self {
183 self.governance_gate = Some(gate);
184 self
185 }
186
187 pub fn set(
191 &self,
192 namespace: &str,
193 key: &str,
194 value: serde_json::Value,
195 changed_by: Pid,
196 ) -> Result<(), KernelError> {
197 #[cfg(feature = "exochain")]
199 if let Some(ref gate) = self.governance_gate {
200 use crate::gate::GateDecision;
201 use crate::governance::{GateEffectKind, with_effect_context};
202 let ctx = with_effect_context(
203 serde_json::json!({
204 "namespace": namespace,
205 "key": key,
206 "changed_by": changed_by,
207 }),
208 &GateEffectKind::ConfigSet.effect_vector(),
209 );
210 let decision = gate.check("config-service", "config.set", &ctx);
211 if let GateDecision::Deny { reason, .. } = decision {
212 return Err(KernelError::GovernanceDenied(format!(
213 "config set denied: {reason}"
214 )));
215 }
216 }
217
218 let config_key = format!("{namespace}/{key}");
219 let old_value = self.configs.get(&config_key).map(|v| v.value().clone());
220 self.configs.insert(config_key, value.clone());
221
222 let change = ConfigChange {
223 namespace: namespace.to_string(),
224 key: key.to_string(),
225 old_value,
226 new_value: Some(value),
227 changed_by,
228 timestamp: Utc::now(),
229 };
230
231 self.notify_subscribers(namespace, &change);
233
234 if let Ok(mut log) = self.change_log.write() {
236 log.push(change);
237 }
238 self.set_count.fetch_add(1, Ordering::Relaxed);
239
240 #[cfg(feature = "exochain")]
241 if let Some(ref cm) = self.chain_manager {
242 cm.append(
243 "config",
244 crate::chain::EVENT_KIND_CONFIG_SET,
245 Some(serde_json::json!({
246 "namespace": namespace,
247 "key": key,
248 "changed_by": changed_by,
249 })),
250 );
251 }
252
253 Ok(())
254 }
255
256 pub fn get(&self, namespace: &str, key: &str) -> Option<serde_json::Value> {
258 let config_key = format!("{namespace}/{key}");
259 self.configs.get(&config_key).map(|v| v.value().clone())
260 }
261
262 pub fn delete(&self, namespace: &str, key: &str, changed_by: Pid) -> Result<(), KernelError> {
264 #[cfg(feature = "exochain")]
266 if let Some(ref gate) = self.governance_gate {
267 use crate::gate::GateDecision;
268 use crate::governance::{GateEffectKind, with_effect_context};
269 let ctx = with_effect_context(
270 serde_json::json!({
271 "namespace": namespace,
272 "key": key,
273 "changed_by": changed_by,
274 }),
275 &GateEffectKind::ConfigDelete.effect_vector(),
276 );
277 let decision = gate.check("config-service", "config.delete", &ctx);
278 if let GateDecision::Deny { reason, .. } = decision {
279 return Err(KernelError::GovernanceDenied(format!(
280 "config delete denied: {reason}"
281 )));
282 }
283 }
284
285 let config_key = format!("{namespace}/{key}");
286 let old_value = self.configs.remove(&config_key).map(|(_, v)| v);
287
288 let change = ConfigChange {
289 namespace: namespace.to_string(),
290 key: key.to_string(),
291 old_value,
292 new_value: None,
293 changed_by,
294 timestamp: Utc::now(),
295 };
296 self.notify_subscribers(namespace, &change);
297
298 #[cfg(feature = "exochain")]
299 if let Some(ref cm) = self.chain_manager {
300 cm.append(
301 "config",
302 crate::chain::EVENT_KIND_CONFIG_DELETE,
303 Some(serde_json::json!({
304 "namespace": namespace,
305 "key": key,
306 "changed_by": changed_by,
307 })),
308 );
309 }
310
311 Ok(())
312 }
313
314 pub fn list_keys(&self, namespace: &str) -> Vec<String> {
316 let prefix = format!("{namespace}/");
317 self.configs
318 .iter()
319 .filter(|e| e.key().starts_with(&prefix))
320 .map(|e| e.key()[prefix.len()..].to_string())
321 .collect()
322 }
323
324 pub fn set_typed(
328 &self,
329 namespace: &str,
330 key: &str,
331 value: ConfigValue,
332 changed_by: Pid,
333 ) -> Result<(), KernelError> {
334 let config_key = format!("{namespace}/{key}");
335 let json_value = value.to_json();
336
337 let old_value = self.configs.get(&config_key).map(|v| v.value().clone());
339 self.configs.insert(config_key.clone(), json_value.clone());
340
341 let entry = ConfigEntry {
342 key: key.to_string(),
343 namespace: namespace.to_string(),
344 value,
345 updated_at: Utc::now(),
346 };
347 self.entries.insert(config_key, entry);
348
349 let change = ConfigChange {
350 namespace: namespace.to_string(),
351 key: key.to_string(),
352 old_value,
353 new_value: Some(json_value),
354 changed_by,
355 timestamp: Utc::now(),
356 };
357 self.notify_subscribers(namespace, &change);
358
359 if let Ok(mut log) = self.change_log.write() {
360 log.push(change);
361 }
362 self.set_count.fetch_add(1, Ordering::Relaxed);
363 Ok(())
364 }
365
366 pub fn get_typed(&self, namespace: &str, key: &str) -> Option<ConfigEntry> {
368 let config_key = format!("{namespace}/{key}");
369 self.entries.get(&config_key).map(|e| e.value().clone())
370 }
371
372 pub fn list(&self, namespace: &str) -> Vec<ConfigEntry> {
374 let prefix = format!("{namespace}/");
375 self.entries
376 .iter()
377 .filter(|e| e.key().starts_with(&prefix))
378 .map(|e| e.value().clone())
379 .collect()
380 }
381
382 pub fn delete_typed(
390 &self,
391 namespace: &str,
392 key: &str,
393 changed_by: Pid,
394 ) -> Result<bool, KernelError> {
395 #[cfg(feature = "exochain")]
397 if let Some(ref gate) = self.governance_gate {
398 use crate::gate::GateDecision;
399 use crate::governance::{GateEffectKind, with_effect_context};
400 let ctx = with_effect_context(
401 serde_json::json!({
402 "namespace": namespace,
403 "key": key,
404 "changed_by": changed_by,
405 }),
406 &GateEffectKind::ConfigDelete.effect_vector(),
407 );
408 let decision = gate.check("config-service", "config.delete_typed", &ctx);
409 if let GateDecision::Deny { reason, .. } = decision {
410 return Err(KernelError::GovernanceDenied(format!(
411 "config delete_typed denied: {reason}"
412 )));
413 }
414 }
415
416 let config_key = format!("{namespace}/{key}");
417 let removed = self.entries.remove(&config_key).is_some();
418 let old_value = self.configs.remove(&config_key).map(|(_, v)| v);
420
421 let change = ConfigChange {
422 namespace: namespace.to_string(),
423 key: key.to_string(),
424 old_value,
425 new_value: None,
426 changed_by,
427 timestamp: Utc::now(),
428 };
429 self.notify_subscribers(namespace, &change);
430 Ok(removed)
431 }
432
433 pub fn subscribe(&self, namespace: &str) -> Arc<RwLock<Vec<ConfigChange>>> {
440 let queue = Arc::new(RwLock::new(Vec::new()));
441 self.subscribers
442 .entry(namespace.to_string())
443 .or_default()
444 .push(queue.clone());
445 queue
446 }
447
448 fn notify_subscribers(&self, namespace: &str, change: &ConfigChange) {
450 if let Some(mut subs) = self.subscribers.get_mut(namespace) {
451 subs.retain(|queue| {
452 if let Ok(mut q) = queue.write() {
453 q.push(change.clone());
454 true
455 } else {
456 false }
458 });
459 }
460 }
461
462 pub fn set_secret(
466 &self,
467 namespace: &str,
468 key: &str,
469 value: &[u8],
470 scoped_to: Vec<Pid>,
471 ) -> Result<(), KernelError> {
472 #[cfg(feature = "exochain")]
474 if let Some(ref gate) = self.governance_gate {
475 use crate::gate::GateDecision;
476 use crate::governance::{GateEffectKind, with_effect_context};
477 let ctx = with_effect_context(
478 serde_json::json!({
479 "namespace": namespace,
480 "key": key,
481 }),
482 &GateEffectKind::ConfigSecretSet.effect_vector(),
483 );
484 let decision = gate.check("config-service", "config.secret.set", &ctx);
485 if let GateDecision::Deny { reason, .. } = decision {
486 return Err(KernelError::GovernanceDenied(format!(
487 "secret set denied: {reason}"
488 )));
489 }
490 }
491
492 let secret_key = format!("{namespace}/{key}");
493
494 let encrypted = self.xor_encrypt(value);
496 self.secrets.insert(secret_key.clone(), encrypted);
497
498 let secret_ref = SecretRef {
499 namespace: namespace.to_string(),
500 key: key.to_string(),
501 expires_at: Utc::now() + Duration::hours(24),
502 scoped_to,
503 };
504 self.secret_refs.insert(secret_key, secret_ref);
505
506 #[cfg(feature = "exochain")]
507 if let Some(ref cm) = self.chain_manager {
508 cm.append(
509 "config",
510 crate::chain::EVENT_KIND_CONFIG_SECRET_SET,
511 Some(serde_json::json!({
512 "namespace": namespace,
513 "key": key,
514 })),
515 );
516 }
517
518 Ok(())
519 }
520
521 pub fn get_secret(
523 &self,
524 namespace: &str,
525 key: &str,
526 requester_pid: Pid,
527 ) -> Result<Vec<u8>, KernelError> {
528 let secret_key = format!("{namespace}/{key}");
529
530 let secret_ref = self
531 .secret_refs
532 .get(&secret_key)
533 .ok_or_else(|| KernelError::Service("secret not found".into()))?;
534
535 if !secret_ref.scoped_to.is_empty() && !secret_ref.scoped_to.contains(&requester_pid) {
537 return Err(KernelError::CapabilityDenied {
538 pid: requester_pid,
539 action: "read_secret".into(),
540 reason: format!(
541 "PID {} not authorized for secret {secret_key}",
542 requester_pid
543 ),
544 });
545 }
546
547 if Utc::now() > secret_ref.expires_at {
549 return Err(KernelError::Service("secret expired".into()));
550 }
551
552 let encrypted = self
553 .secrets
554 .get(&secret_key)
555 .ok_or_else(|| KernelError::Service("secret data missing".into()))?;
556
557 Ok(self.xor_decrypt(&encrypted))
558 }
559
560 fn xor_encrypt(&self, data: &[u8]) -> Vec<u8> {
562 data.iter()
563 .enumerate()
564 .map(|(i, b)| b ^ self.encryption_key[i % 32])
565 .collect()
566 }
567
568 fn xor_decrypt(&self, data: &[u8]) -> Vec<u8> {
570 self.xor_encrypt(data)
572 }
573
574 pub fn change_log(&self) -> Vec<ConfigChange> {
576 self.change_log
577 .read()
578 .map(|l| l.clone())
579 .unwrap_or_default()
580 }
581
582 pub fn set_count(&self) -> u64 {
584 self.set_count.load(Ordering::Relaxed)
585 }
586}
587
588#[async_trait]
589impl SystemService for ConfigService {
590 fn name(&self) -> &str {
591 "config-service"
592 }
593
594 fn service_type(&self) -> ServiceType {
595 ServiceType::Core
596 }
597
598 async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
599 info!("config service started");
600 Ok(())
601 }
602
603 async fn stop(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
604 info!(
605 configs = self.configs.len(),
606 secrets = self.secrets.len(),
607 "config service stopped"
608 );
609 Ok(())
610 }
611
612 async fn health_check(&self) -> HealthStatus {
613 HealthStatus::Healthy
614 }
615}
616
617#[cfg(test)]
620mod tests {
621 use super::*;
622
623 fn pid(n: u64) -> Pid {
624 n
625 }
626
627 #[test]
628 fn set_and_get_config() {
629 let svc = ConfigService::new_default();
630 svc.set("app", "timeout", serde_json::json!(30), pid(1))
631 .unwrap();
632 let val = svc.get("app", "timeout").unwrap();
633 assert_eq!(val, serde_json::json!(30));
634 }
635
636 #[test]
637 fn get_nonexistent_returns_none() {
638 let svc = ConfigService::new_default();
639 assert!(svc.get("app", "missing").is_none());
640 }
641
642 #[test]
643 fn delete_config() {
644 let svc = ConfigService::new_default();
645 svc.set("app", "key", serde_json::json!("val"), pid(1))
646 .unwrap();
647 svc.delete("app", "key", pid(1)).unwrap();
648 assert!(svc.get("app", "key").is_none());
649 }
650
651 #[test]
652 fn list_keys_in_namespace() {
653 let svc = ConfigService::new_default();
654 svc.set("ns", "a", serde_json::json!(1), pid(1)).unwrap();
655 svc.set("ns", "b", serde_json::json!(2), pid(1)).unwrap();
656 svc.set("other", "c", serde_json::json!(3), pid(1)).unwrap();
657 let mut keys = svc.list_keys("ns");
658 keys.sort();
659 assert_eq!(keys, vec!["a", "b"]);
660 }
661
662 #[test]
663 fn config_change_notification() {
664 let svc = ConfigService::new_default();
665 let sub = svc.subscribe("watch");
666 svc.set("watch", "flag", serde_json::json!(true), pid(1))
667 .unwrap();
668 let changes = sub.read().unwrap();
669 assert_eq!(changes.len(), 1);
670 assert_eq!(changes[0].key, "flag");
671 assert_eq!(changes[0].new_value, Some(serde_json::json!(true)));
672 }
673
674 #[test]
675 fn config_change_includes_old_value() {
676 let svc = ConfigService::new_default();
677 let sub = svc.subscribe("ver");
678 svc.set("ver", "v", serde_json::json!(1), pid(1)).unwrap();
679 svc.set("ver", "v", serde_json::json!(2), pid(1)).unwrap();
680 let changes = sub.read().unwrap();
681 assert_eq!(changes.len(), 2);
682 assert_eq!(changes[1].old_value, Some(serde_json::json!(1)));
683 assert_eq!(changes[1].new_value, Some(serde_json::json!(2)));
684 }
685
686 #[test]
687 fn secret_set_and_get() {
688 let key = [0xAB; 32];
689 let svc = ConfigService::new(key);
690 svc.set_secret("creds", "api_key", b"secret123", vec![pid(1)])
691 .unwrap();
692 let val = svc.get_secret("creds", "api_key", pid(1)).unwrap();
693 assert_eq!(val, b"secret123");
694 }
695
696 #[test]
697 fn secret_encrypted_at_rest() {
698 let key = [0xAB; 32];
699 let svc = ConfigService::new(key);
700 svc.set_secret("creds", "pass", b"plaintext", vec![pid(1)])
701 .unwrap();
702 let stored = svc.secrets.get("creds/pass").unwrap();
704 assert_ne!(stored.as_slice(), b"plaintext");
705 }
706
707 #[test]
708 fn unauthorized_pid_cannot_read_secret() {
709 let svc = ConfigService::new_default();
710 svc.set_secret("creds", "key", b"val", vec![pid(1)])
711 .unwrap();
712 let result = svc.get_secret("creds", "key", pid(99));
713 assert!(result.is_err());
714 let err = result.unwrap_err().to_string();
715 assert!(
716 err.contains("denied") || err.contains("authorized"),
717 "got: {err}"
718 );
719 }
720
721 #[test]
722 fn empty_scope_allows_any_pid() {
723 let svc = ConfigService::new_default();
724 svc.set_secret("open", "key", b"val", vec![]).unwrap();
725 let val = svc.get_secret("open", "key", pid(42)).unwrap();
726 assert_eq!(val, b"val");
727 }
728
729 #[test]
730 fn change_log_recorded() {
731 let svc = ConfigService::new_default();
732 svc.set("ns", "k", serde_json::json!("v"), pid(1)).unwrap();
733 let log = svc.change_log();
734 assert_eq!(log.len(), 1);
735 assert_eq!(log[0].namespace, "ns");
736 }
737
738 #[tokio::test]
739 async fn system_service_impl() {
740 let svc = ConfigService::new_default();
741 assert_eq!(svc.name(), "config-service");
742 assert_eq!(svc.service_type(), ServiceType::Core);
743 svc.start().await.unwrap();
744 assert_eq!(svc.health_check().await, HealthStatus::Healthy);
745 svc.stop().await.unwrap();
746 }
747
748 #[test]
751 fn typed_set_get_roundtrip() {
752 let svc = ConfigService::new_default();
753 svc.set_typed("app", "name", ConfigValue::Text("myapp".into()), pid(1))
754 .unwrap();
755 let entry = svc.get_typed("app", "name").unwrap();
756 assert_eq!(entry.value, ConfigValue::Text("myapp".into()));
757 assert_eq!(entry.namespace, "app");
758 assert_eq!(entry.key, "name");
759 }
760
761 #[test]
762 fn typed_get_nonexistent_returns_none() {
763 let svc = ConfigService::new_default();
764 assert!(svc.get_typed("app", "missing").is_none());
765 }
766
767 #[test]
768 fn typed_list_returns_all_in_namespace() {
769 let svc = ConfigService::new_default();
770 svc.set_typed("db", "host", ConfigValue::Text("localhost".into()), pid(1))
771 .unwrap();
772 svc.set_typed("db", "port", ConfigValue::Integer(5432), pid(1))
773 .unwrap();
774 svc.set_typed("cache", "ttl", ConfigValue::Integer(60), pid(1))
775 .unwrap();
776
777 let mut entries = svc.list("db");
778 entries.sort_by(|a, b| a.key.cmp(&b.key));
779 assert_eq!(entries.len(), 2);
780 assert_eq!(entries[0].key, "host");
781 assert_eq!(entries[1].key, "port");
782 }
783
784 #[test]
785 fn typed_delete_removes_entry() {
786 let svc = ConfigService::new_default();
787 svc.set_typed("ns", "k", ConfigValue::Boolean(true), pid(1))
788 .unwrap();
789 assert!(svc.delete_typed("ns", "k", pid(1)).unwrap());
790 assert!(svc.get_typed("ns", "k").is_none());
791 assert!(!svc.delete_typed("ns", "k", pid(1)).unwrap());
793 }
794
795 #[test]
796 fn typed_subscribe_receives_change_notification() {
797 let svc = ConfigService::new_default();
798 let sub = svc.subscribe("typed-ns");
799 svc.set_typed("typed-ns", "flag", ConfigValue::Boolean(true), pid(1))
800 .unwrap();
801 let changes = sub.read().unwrap();
802 assert_eq!(changes.len(), 1);
803 assert_eq!(changes[0].key, "flag");
804 assert_eq!(changes[0].new_value, Some(serde_json::json!(true)));
805 }
806
807 #[test]
808 fn typed_set_updates_existing_value() {
809 let svc = ConfigService::new_default();
810 svc.set_typed("app", "level", ConfigValue::Integer(1), pid(1))
811 .unwrap();
812 svc.set_typed("app", "level", ConfigValue::Integer(2), pid(1))
813 .unwrap();
814 let entry = svc.get_typed("app", "level").unwrap();
815 assert_eq!(entry.value, ConfigValue::Integer(2));
816 }
817
818 #[test]
819 fn typed_namespace_isolation() {
820 let svc = ConfigService::new_default();
821 svc.set_typed("alpha", "key", ConfigValue::Text("a".into()), pid(1))
822 .unwrap();
823 svc.set_typed("beta", "key", ConfigValue::Text("b".into()), pid(1))
824 .unwrap();
825
826 let a = svc.get_typed("alpha", "key").unwrap();
827 let b = svc.get_typed("beta", "key").unwrap();
828 assert_eq!(a.value, ConfigValue::Text("a".into()));
829 assert_eq!(b.value, ConfigValue::Text("b".into()));
830
831 assert_eq!(svc.list("alpha").len(), 1);
833 assert_eq!(svc.list("beta").len(), 1);
834 assert_eq!(svc.list("gamma").len(), 0);
835 }
836
837 #[test]
838 fn typed_all_value_variants() {
839 let svc = ConfigService::new_default();
840
841 svc.set_typed("t", "text", ConfigValue::Text("hello".into()), pid(1))
842 .unwrap();
843 svc.set_typed("t", "int", ConfigValue::Integer(42), pid(1))
844 .unwrap();
845 svc.set_typed("t", "float", ConfigValue::Float(2.5), pid(1))
846 .unwrap();
847 svc.set_typed("t", "bool", ConfigValue::Boolean(false), pid(1))
848 .unwrap();
849 svc.set_typed(
850 "t",
851 "json",
852 ConfigValue::Json(serde_json::json!({"a": 1})),
853 pid(1),
854 )
855 .unwrap();
856
857 assert_eq!(
858 svc.get_typed("t", "text").unwrap().value,
859 ConfigValue::Text("hello".into())
860 );
861 assert_eq!(
862 svc.get_typed("t", "int").unwrap().value,
863 ConfigValue::Integer(42)
864 );
865 assert_eq!(
866 svc.get_typed("t", "float").unwrap().value,
867 ConfigValue::Float(2.5)
868 );
869 assert_eq!(
870 svc.get_typed("t", "bool").unwrap().value,
871 ConfigValue::Boolean(false)
872 );
873 assert_eq!(
874 svc.get_typed("t", "json").unwrap().value,
875 ConfigValue::Json(serde_json::json!({"a": 1}))
876 );
877 }
878}