1use std::collections::HashMap;
7use std::sync::Arc;
8
9use crate::error::{Error, Result};
10use crate::value::Value;
11
12#[derive(Debug, Clone)]
14pub struct ResolvedValue {
15 pub value: Value,
17 pub sensitive: bool,
19}
20
21impl ResolvedValue {
22 pub fn new(value: impl Into<Value>) -> Self {
24 Self {
25 value: value.into(),
26 sensitive: false,
27 }
28 }
29
30 pub fn sensitive(value: impl Into<Value>) -> Self {
32 Self {
33 value: value.into(),
34 sensitive: true,
35 }
36 }
37}
38
39impl From<Value> for ResolvedValue {
40 fn from(value: Value) -> Self {
41 ResolvedValue::new(value)
42 }
43}
44
45impl From<String> for ResolvedValue {
46 fn from(s: String) -> Self {
47 ResolvedValue::new(Value::String(s))
48 }
49}
50
51impl From<&str> for ResolvedValue {
52 fn from(s: &str) -> Self {
53 ResolvedValue::new(Value::String(s.to_string()))
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct ResolverContext {
60 pub config_path: String,
62 pub config_root: Option<Arc<Value>>,
64 pub base_path: Option<std::path::PathBuf>,
66 pub resolution_stack: Vec<String>,
68}
69
70impl ResolverContext {
71 pub fn new(config_path: impl Into<String>) -> Self {
73 Self {
74 config_path: config_path.into(),
75 config_root: None,
76 base_path: None,
77 resolution_stack: Vec::new(),
78 }
79 }
80
81 pub fn with_config_root(mut self, root: Arc<Value>) -> Self {
83 self.config_root = Some(root);
84 self
85 }
86
87 pub fn with_base_path(mut self, path: std::path::PathBuf) -> Self {
89 self.base_path = Some(path);
90 self
91 }
92
93 pub fn would_cause_cycle(&self, path: &str) -> bool {
95 self.resolution_stack.contains(&path.to_string())
96 }
97
98 pub fn push_resolution(&mut self, path: &str) {
100 self.resolution_stack.push(path.to_string());
101 }
102
103 pub fn pop_resolution(&mut self) {
105 self.resolution_stack.pop();
106 }
107
108 pub fn get_resolution_chain(&self) -> Vec<String> {
110 self.resolution_stack.clone()
111 }
112}
113
114pub trait Resolver: Send + Sync {
116 fn resolve(
123 &self,
124 args: &[String],
125 kwargs: &HashMap<String, String>,
126 ctx: &ResolverContext,
127 ) -> Result<ResolvedValue>;
128
129 fn name(&self) -> &str;
131}
132
133pub struct FnResolver<F>
135where
136 F: Fn(&[String], &HashMap<String, String>, &ResolverContext) -> Result<ResolvedValue>
137 + Send
138 + Sync,
139{
140 name: String,
141 func: F,
142}
143
144impl<F> FnResolver<F>
145where
146 F: Fn(&[String], &HashMap<String, String>, &ResolverContext) -> Result<ResolvedValue>
147 + Send
148 + Sync,
149{
150 pub fn new(name: impl Into<String>, func: F) -> Self {
152 Self {
153 name: name.into(),
154 func,
155 }
156 }
157}
158
159impl<F> Resolver for FnResolver<F>
160where
161 F: Fn(&[String], &HashMap<String, String>, &ResolverContext) -> Result<ResolvedValue>
162 + Send
163 + Sync,
164{
165 fn resolve(
166 &self,
167 args: &[String],
168 kwargs: &HashMap<String, String>,
169 ctx: &ResolverContext,
170 ) -> Result<ResolvedValue> {
171 (self.func)(args, kwargs, ctx)
172 }
173
174 fn name(&self) -> &str {
175 &self.name
176 }
177}
178
179pub struct ResolverRegistry {
181 resolvers: HashMap<String, Arc<dyn Resolver>>,
182}
183
184impl Default for ResolverRegistry {
185 fn default() -> Self {
186 Self::new()
187 }
188}
189
190impl ResolverRegistry {
191 pub fn new() -> Self {
193 Self {
194 resolvers: HashMap::new(),
195 }
196 }
197
198 pub fn with_builtins() -> Self {
200 let mut registry = Self::new();
201 registry.register_builtin_resolvers();
202 registry
203 }
204
205 fn register_builtin_resolvers(&mut self) {
207 self.register(Arc::new(FnResolver::new("env", env_resolver)));
209 self.register(Arc::new(FnResolver::new("file", file_resolver)));
211 self.register(Arc::new(FnResolver::new("http", http_resolver)));
213 }
214
215 pub fn register(&mut self, resolver: Arc<dyn Resolver>) {
217 self.resolvers.insert(resolver.name().to_string(), resolver);
218 }
219
220 pub fn register_fn<F>(&mut self, name: impl Into<String>, func: F)
222 where
223 F: Fn(&[String], &HashMap<String, String>, &ResolverContext) -> Result<ResolvedValue>
224 + Send
225 + Sync
226 + 'static,
227 {
228 let name = name.into();
229 self.register(Arc::new(FnResolver::new(name, func)));
230 }
231
232 pub fn get(&self, name: &str) -> Option<&Arc<dyn Resolver>> {
234 self.resolvers.get(name)
235 }
236
237 pub fn contains(&self, name: &str) -> bool {
239 self.resolvers.contains_key(name)
240 }
241
242 pub fn resolve(
244 &self,
245 resolver_name: &str,
246 args: &[String],
247 kwargs: &HashMap<String, String>,
248 ctx: &ResolverContext,
249 ) -> Result<ResolvedValue> {
250 let resolver = self
251 .resolvers
252 .get(resolver_name)
253 .ok_or_else(|| Error::unknown_resolver(resolver_name, Some(ctx.config_path.clone())))?;
254
255 resolver.resolve(args, kwargs, ctx)
256 }
257}
258
259fn env_resolver(
267 args: &[String],
268 kwargs: &HashMap<String, String>,
269 ctx: &ResolverContext,
270) -> Result<ResolvedValue> {
271 if args.is_empty() {
272 return Err(Error::parse("env resolver requires a variable name")
273 .with_path(ctx.config_path.clone()));
274 }
275
276 let var_name = &args[0];
277 let default_value = args.get(1);
278
279 let is_sensitive = kwargs
281 .get("sensitive")
282 .map(|v| v.eq_ignore_ascii_case("true"))
283 .unwrap_or(false);
284
285 match std::env::var(var_name) {
286 Ok(value) => {
287 let resolved_value = Value::String(value);
288 if is_sensitive {
289 Ok(ResolvedValue::sensitive(resolved_value))
290 } else {
291 Ok(ResolvedValue::new(resolved_value))
292 }
293 }
294 Err(_) => {
295 if let Some(default) = default_value {
296 let resolved_value = Value::String(default.clone());
297 if is_sensitive {
298 Ok(ResolvedValue::sensitive(resolved_value))
299 } else {
300 Ok(ResolvedValue::new(resolved_value))
301 }
302 } else {
303 Err(Error::env_not_found(
304 var_name,
305 Some(ctx.config_path.clone()),
306 ))
307 }
308 }
309 }
310}
311
312fn file_resolver(
314 args: &[String],
315 kwargs: &HashMap<String, String>,
316 ctx: &ResolverContext,
317) -> Result<ResolvedValue> {
318 use std::path::Path;
319
320 if args.is_empty() {
321 return Err(
322 Error::parse("file resolver requires a file path").with_path(ctx.config_path.clone())
323 );
324 }
325
326 let file_path_str = &args[0];
327 let parse_mode = kwargs.get("parse").map(|s| s.as_str()).unwrap_or("auto");
328
329 let file_path = if Path::new(file_path_str).is_relative() {
331 if let Some(base) = &ctx.base_path {
332 base.join(file_path_str)
333 } else {
334 std::path::PathBuf::from(file_path_str)
335 }
336 } else {
337 std::path::PathBuf::from(file_path_str)
338 };
339
340 let content = std::fs::read_to_string(&file_path)
342 .map_err(|_| Error::file_not_found(file_path_str, Some(ctx.config_path.clone())))?;
343
344 let actual_parse_mode = if parse_mode == "auto" {
346 match file_path.extension().and_then(|e| e.to_str()) {
348 Some("yaml") | Some("yml") => "yaml",
349 Some("json") => "json",
350 _ => "text",
351 }
352 } else {
353 parse_mode
354 };
355
356 match actual_parse_mode {
358 "yaml" => {
359 let value: Value = serde_yaml::from_str(&content).map_err(|e| {
360 Error::parse(format!("Failed to parse YAML: {}", e))
361 .with_path(ctx.config_path.clone())
362 })?;
363 Ok(ResolvedValue::new(value))
364 }
365 "json" => {
366 let value: Value = serde_json::from_str(&content).map_err(|e| {
367 Error::parse(format!("Failed to parse JSON: {}", e))
368 .with_path(ctx.config_path.clone())
369 })?;
370 Ok(ResolvedValue::new(value))
371 }
372 _ => {
373 Ok(ResolvedValue::new(Value::String(content)))
375 }
376 }
377}
378
379fn http_resolver(
386 args: &[String],
387 _kwargs: &HashMap<String, String>,
388 ctx: &ResolverContext,
389) -> Result<ResolvedValue> {
390 if args.is_empty() {
391 return Err(Error::parse("http resolver requires a URL").with_path(ctx.config_path.clone()));
392 }
393
394 let url = &args[0];
395
396 Err(Error {
402 kind: crate::error::ErrorKind::Resolver(crate::error::ResolverErrorKind::HttpDisabled),
403 path: Some(ctx.config_path.clone()),
404 source_location: None,
405 help: Some(format!(
406 "Enable HTTP resolver with Config.load(..., allow_http=True)\nURL: {}",
407 url
408 )),
409 cause: None,
410 })
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416
417 #[test]
418 fn test_env_resolver_with_value() {
419 std::env::set_var("HOLOCONF_TEST_VAR", "test_value");
420
421 let ctx = ResolverContext::new("test.path");
422 let args = vec!["HOLOCONF_TEST_VAR".to_string()];
423 let kwargs = HashMap::new();
424
425 let result = env_resolver(&args, &kwargs, &ctx).unwrap();
426 assert_eq!(result.value.as_str(), Some("test_value"));
427 assert!(!result.sensitive);
428
429 std::env::remove_var("HOLOCONF_TEST_VAR");
430 }
431
432 #[test]
433 fn test_env_resolver_with_default() {
434 std::env::remove_var("HOLOCONF_NONEXISTENT_VAR");
436
437 let ctx = ResolverContext::new("test.path");
438 let args = vec![
439 "HOLOCONF_NONEXISTENT_VAR".to_string(),
440 "default_value".to_string(),
441 ];
442 let kwargs = HashMap::new();
443
444 let result = env_resolver(&args, &kwargs, &ctx).unwrap();
445 assert_eq!(result.value.as_str(), Some("default_value"));
446 }
447
448 #[test]
449 fn test_env_resolver_missing_no_default() {
450 std::env::remove_var("HOLOCONF_MISSING_VAR");
451
452 let ctx = ResolverContext::new("test.path");
453 let args = vec!["HOLOCONF_MISSING_VAR".to_string()];
454 let kwargs = HashMap::new();
455
456 let result = env_resolver(&args, &kwargs, &ctx);
457 assert!(result.is_err());
458 }
459
460 #[test]
461 fn test_env_resolver_sensitive_kwarg() {
462 std::env::set_var("HOLOCONF_SENSITIVE_VAR", "secret_value");
463
464 let ctx = ResolverContext::new("test.path");
465 let args = vec!["HOLOCONF_SENSITIVE_VAR".to_string()];
466 let mut kwargs = HashMap::new();
467 kwargs.insert("sensitive".to_string(), "true".to_string());
468
469 let result = env_resolver(&args, &kwargs, &ctx).unwrap();
470 assert_eq!(result.value.as_str(), Some("secret_value"));
471 assert!(result.sensitive);
472
473 std::env::remove_var("HOLOCONF_SENSITIVE_VAR");
474 }
475
476 #[test]
477 fn test_env_resolver_sensitive_false() {
478 std::env::set_var("HOLOCONF_NON_SENSITIVE", "public_value");
479
480 let ctx = ResolverContext::new("test.path");
481 let args = vec!["HOLOCONF_NON_SENSITIVE".to_string()];
482 let mut kwargs = HashMap::new();
483 kwargs.insert("sensitive".to_string(), "false".to_string());
484
485 let result = env_resolver(&args, &kwargs, &ctx).unwrap();
486 assert_eq!(result.value.as_str(), Some("public_value"));
487 assert!(!result.sensitive);
488
489 std::env::remove_var("HOLOCONF_NON_SENSITIVE");
490 }
491
492 #[test]
493 fn test_env_resolver_sensitive_with_default() {
494 std::env::remove_var("HOLOCONF_SENSITIVE_DEFAULT");
495
496 let ctx = ResolverContext::new("test.path");
497 let args = vec![
498 "HOLOCONF_SENSITIVE_DEFAULT".to_string(),
499 "default_secret".to_string(),
500 ];
501 let mut kwargs = HashMap::new();
502 kwargs.insert("sensitive".to_string(), "true".to_string());
503
504 let result = env_resolver(&args, &kwargs, &ctx).unwrap();
505 assert_eq!(result.value.as_str(), Some("default_secret"));
506 assert!(result.sensitive);
507 }
508
509 #[test]
510 fn test_resolver_registry() {
511 let registry = ResolverRegistry::with_builtins();
512
513 assert!(registry.contains("env"));
514 assert!(!registry.contains("nonexistent"));
515 }
516
517 #[test]
518 fn test_custom_resolver() {
519 let mut registry = ResolverRegistry::new();
520
521 registry.register_fn("custom", |args, _kwargs, _ctx| {
522 let value = args.first().cloned().unwrap_or_default();
523 Ok(ResolvedValue::new(Value::String(format!(
524 "custom:{}",
525 value
526 ))))
527 });
528
529 let ctx = ResolverContext::new("test");
530 let result = registry
531 .resolve("custom", &["arg".to_string()], &HashMap::new(), &ctx)
532 .unwrap();
533
534 assert_eq!(result.value.as_str(), Some("custom:arg"));
535 }
536
537 #[test]
538 fn test_resolved_value_sensitivity() {
539 let non_sensitive = ResolvedValue::new("public");
540 assert!(!non_sensitive.sensitive);
541
542 let sensitive = ResolvedValue::sensitive("secret");
543 assert!(sensitive.sensitive);
544 }
545
546 #[test]
547 fn test_resolver_context_cycle_detection() {
548 let mut ctx = ResolverContext::new("root");
549 ctx.push_resolution("a");
550 ctx.push_resolution("b");
551
552 assert!(ctx.would_cause_cycle("a"));
553 assert!(ctx.would_cause_cycle("b"));
554 assert!(!ctx.would_cause_cycle("c"));
555
556 ctx.pop_resolution();
557 assert!(!ctx.would_cause_cycle("b"));
558 }
559
560 #[test]
561 fn test_file_resolver() {
562 use std::io::Write;
563
564 let temp_dir = std::env::temp_dir();
566 let test_file = temp_dir.join("holoconf_test_file.txt");
567 {
568 let mut file = std::fs::File::create(&test_file).unwrap();
569 writeln!(file, "test content").unwrap();
570 }
571
572 let mut ctx = ResolverContext::new("test.path");
573 ctx.base_path = Some(temp_dir.clone());
574
575 let args = vec!["holoconf_test_file.txt".to_string()];
576 let mut kwargs = HashMap::new();
577 kwargs.insert("parse".to_string(), "text".to_string());
578
579 let result = file_resolver(&args, &kwargs, &ctx).unwrap();
580 assert!(result.value.as_str().unwrap().contains("test content"));
581 assert!(!result.sensitive);
582
583 std::fs::remove_file(test_file).ok();
585 }
586
587 #[test]
588 fn test_file_resolver_yaml() {
589 use std::io::Write;
590
591 let temp_dir = std::env::temp_dir();
593 let test_file = temp_dir.join("holoconf_test.yaml");
594 {
595 let mut file = std::fs::File::create(&test_file).unwrap();
596 writeln!(file, "key: value").unwrap();
597 writeln!(file, "number: 42").unwrap();
598 }
599
600 let mut ctx = ResolverContext::new("test.path");
601 ctx.base_path = Some(temp_dir.clone());
602
603 let args = vec!["holoconf_test.yaml".to_string()];
604 let kwargs = HashMap::new();
605
606 let result = file_resolver(&args, &kwargs, &ctx).unwrap();
607 assert!(result.value.is_mapping());
608
609 std::fs::remove_file(test_file).ok();
611 }
612
613 #[test]
614 fn test_file_resolver_not_found() {
615 let ctx = ResolverContext::new("test.path");
616 let args = vec!["nonexistent_file.txt".to_string()];
617 let kwargs = HashMap::new();
618
619 let result = file_resolver(&args, &kwargs, &ctx);
620 assert!(result.is_err());
621 }
622
623 #[test]
624 fn test_registry_with_file() {
625 let registry = ResolverRegistry::with_builtins();
626 assert!(registry.contains("file"));
627 }
628
629 #[test]
630 fn test_http_resolver_disabled() {
631 let ctx = ResolverContext::new("test.path");
632 let args = vec!["https://example.com/config.yaml".to_string()];
633 let kwargs = HashMap::new();
634
635 let result = http_resolver(&args, &kwargs, &ctx);
636 assert!(result.is_err());
637
638 let err = result.unwrap_err();
639 let display = format!("{}", err);
640 assert!(display.contains("HTTP resolver is disabled"));
641 }
642
643 #[test]
644 fn test_registry_with_http() {
645 let registry = ResolverRegistry::with_builtins();
646 assert!(registry.contains("http"));
647 }
648
649 #[test]
652 fn test_env_resolver_no_args() {
653 let ctx = ResolverContext::new("test.path");
654 let args = vec![];
655 let kwargs = HashMap::new();
656
657 let result = env_resolver(&args, &kwargs, &ctx);
658 assert!(result.is_err());
659 let err = result.unwrap_err();
660 assert!(err.to_string().contains("requires"));
661 }
662
663 #[test]
664 fn test_file_resolver_no_args() {
665 let ctx = ResolverContext::new("test.path");
666 let args = vec![];
667 let kwargs = HashMap::new();
668
669 let result = file_resolver(&args, &kwargs, &ctx);
670 assert!(result.is_err());
671 let err = result.unwrap_err();
672 assert!(err.to_string().contains("requires"));
673 }
674
675 #[test]
676 fn test_http_resolver_no_args() {
677 let ctx = ResolverContext::new("test.path");
678 let args = vec![];
679 let kwargs = HashMap::new();
680
681 let result = http_resolver(&args, &kwargs, &ctx);
682 assert!(result.is_err());
683 let err = result.unwrap_err();
684 assert!(err.to_string().contains("requires"));
685 }
686
687 #[test]
688 fn test_unknown_resolver() {
689 let registry = ResolverRegistry::with_builtins();
690 let ctx = ResolverContext::new("test.path");
691
692 let result = registry.resolve("unknown_resolver", &[], &HashMap::new(), &ctx);
693 assert!(result.is_err());
694 let err = result.unwrap_err();
695 assert!(err.to_string().contains("unknown_resolver"));
696 }
697
698 #[test]
699 fn test_resolved_value_from_traits() {
700 let from_value: ResolvedValue = Value::String("test".to_string()).into();
701 assert_eq!(from_value.value.as_str(), Some("test"));
702 assert!(!from_value.sensitive);
703
704 let from_string: ResolvedValue = "hello".to_string().into();
705 assert_eq!(from_string.value.as_str(), Some("hello"));
706
707 let from_str: ResolvedValue = "world".into();
708 assert_eq!(from_str.value.as_str(), Some("world"));
709 }
710
711 #[test]
712 fn test_resolver_context_with_base_path() {
713 let ctx = ResolverContext::new("test").with_base_path(std::path::PathBuf::from("/tmp"));
714 assert_eq!(ctx.base_path, Some(std::path::PathBuf::from("/tmp")));
715 }
716
717 #[test]
718 fn test_resolver_context_with_config_root() {
719 use std::sync::Arc;
720 let root = Arc::new(Value::String("root".to_string()));
721 let ctx = ResolverContext::new("test").with_config_root(root.clone());
722 assert!(ctx.config_root.is_some());
723 }
724
725 #[test]
726 fn test_resolver_context_resolution_chain() {
727 let mut ctx = ResolverContext::new("root");
728 ctx.push_resolution("a");
729 ctx.push_resolution("b");
730 ctx.push_resolution("c");
731
732 let chain = ctx.get_resolution_chain();
733 assert_eq!(chain, vec!["a", "b", "c"]);
734 }
735
736 #[test]
737 fn test_registry_get_resolver() {
738 let registry = ResolverRegistry::with_builtins();
739
740 let env_resolver = registry.get("env");
741 assert!(env_resolver.is_some());
742 assert_eq!(env_resolver.unwrap().name(), "env");
743
744 let missing = registry.get("nonexistent");
745 assert!(missing.is_none());
746 }
747
748 #[test]
749 fn test_registry_default() {
750 let registry = ResolverRegistry::default();
751 assert!(!registry.contains("env"));
753 }
754
755 #[test]
756 fn test_fn_resolver_name() {
757 let resolver = FnResolver::new("my_resolver", |_, _, _| Ok(ResolvedValue::new("test")));
758 assert_eq!(resolver.name(), "my_resolver");
759 }
760
761 #[test]
762 fn test_file_resolver_json() {
763 use std::io::Write;
764
765 let temp_dir = std::env::temp_dir();
767 let test_file = temp_dir.join("holoconf_test.json");
768 {
769 let mut file = std::fs::File::create(&test_file).unwrap();
770 writeln!(file, r#"{{"key": "value", "number": 42}}"#).unwrap();
771 }
772
773 let mut ctx = ResolverContext::new("test.path");
774 ctx.base_path = Some(temp_dir.clone());
775
776 let args = vec!["holoconf_test.json".to_string()];
777 let kwargs = HashMap::new();
778
779 let result = file_resolver(&args, &kwargs, &ctx).unwrap();
780 assert!(result.value.is_mapping());
781
782 std::fs::remove_file(test_file).ok();
784 }
785
786 #[test]
787 fn test_file_resolver_absolute_path() {
788 use std::io::Write;
789
790 let temp_dir = std::env::temp_dir();
792 let test_file = temp_dir.join("holoconf_abs_test.txt");
793 {
794 let mut file = std::fs::File::create(&test_file).unwrap();
795 writeln!(file, "absolute path content").unwrap();
796 }
797
798 let ctx = ResolverContext::new("test.path");
799 let args = vec![test_file.to_string_lossy().to_string()];
801 let mut kwargs = HashMap::new();
802 kwargs.insert("parse".to_string(), "text".to_string());
803
804 let result = file_resolver(&args, &kwargs, &ctx).unwrap();
805 assert!(result
806 .value
807 .as_str()
808 .unwrap()
809 .contains("absolute path content"));
810
811 std::fs::remove_file(test_file).ok();
813 }
814
815 #[test]
816 fn test_file_resolver_invalid_yaml() {
817 use std::io::Write;
818
819 let temp_dir = std::env::temp_dir();
821 let test_file = temp_dir.join("holoconf_invalid.yaml");
822 {
823 let mut file = std::fs::File::create(&test_file).unwrap();
824 writeln!(file, "key: [invalid").unwrap();
825 }
826
827 let mut ctx = ResolverContext::new("test.path");
828 ctx.base_path = Some(temp_dir.clone());
829
830 let args = vec!["holoconf_invalid.yaml".to_string()];
831 let kwargs = HashMap::new();
832
833 let result = file_resolver(&args, &kwargs, &ctx);
834 assert!(result.is_err());
835 let err = result.unwrap_err();
836 assert!(err.to_string().contains("parse") || err.to_string().contains("YAML"));
837
838 std::fs::remove_file(test_file).ok();
840 }
841
842 #[test]
843 fn test_file_resolver_invalid_json() {
844 use std::io::Write;
845
846 let temp_dir = std::env::temp_dir();
848 let test_file = temp_dir.join("holoconf_invalid.json");
849 {
850 let mut file = std::fs::File::create(&test_file).unwrap();
851 writeln!(file, "{{invalid json}}").unwrap();
852 }
853
854 let mut ctx = ResolverContext::new("test.path");
855 ctx.base_path = Some(temp_dir.clone());
856
857 let args = vec!["holoconf_invalid.json".to_string()];
858 let kwargs = HashMap::new();
859
860 let result = file_resolver(&args, &kwargs, &ctx);
861 assert!(result.is_err());
862 let err = result.unwrap_err();
863 assert!(err.to_string().contains("parse") || err.to_string().contains("JSON"));
864
865 std::fs::remove_file(test_file).ok();
867 }
868
869 #[test]
870 fn test_file_resolver_unknown_extension() {
871 use std::io::Write;
872
873 let temp_dir = std::env::temp_dir();
875 let test_file = temp_dir.join("holoconf_test.xyz");
876 {
877 let mut file = std::fs::File::create(&test_file).unwrap();
878 writeln!(file, "plain text content").unwrap();
879 }
880
881 let mut ctx = ResolverContext::new("test.path");
882 ctx.base_path = Some(temp_dir.clone());
883
884 let args = vec!["holoconf_test.xyz".to_string()];
885 let kwargs = HashMap::new();
886
887 let result = file_resolver(&args, &kwargs, &ctx).unwrap();
888 assert!(result
890 .value
891 .as_str()
892 .unwrap()
893 .contains("plain text content"));
894
895 std::fs::remove_file(test_file).ok();
897 }
898}