muster/adapter/cli/
check.rs1use std::path::PathBuf;
2
3use super::error::CliError;
4use crate::{
5 adapter::config::YamlConfigSource,
6 domain::{config::ConfigError, port::ConfigSource},
7};
8
9const CHAIN_SEPARATOR: &str = ": ";
11pub const VALID_SUFFIX: &str = "is valid";
13
14#[derive(Debug)]
16pub enum CheckOutcome {
17 Valid,
19 Invalid(String),
21}
22
23pub fn check(config_path: PathBuf) -> Result<CheckOutcome, CliError> {
30 let source = YamlConfigSource::builder().path(config_path).build();
31 match source.load() {
32 Ok(_) => Ok(CheckOutcome::Valid),
33 Err(error @ ConfigError::Read { .. }) => Err(CliError::Config(error)),
34 Err(error) => Ok(CheckOutcome::Invalid(error_chain(&error))),
35 }
36}
37
38pub fn error_chain(error: &dyn std::error::Error) -> String {
41 let mut report = error.to_string();
42 let mut source = error.source();
43 while let Some(cause) = source {
44 let text = cause.to_string();
45 if !report.contains(&text) {
48 report.push_str(CHAIN_SEPARATOR);
49 report.push_str(&text);
50 }
51 source = cause.source();
52 }
53 report
54}
55
56#[cfg(test)]
57mod tests {
58 use std::{fs, path::PathBuf};
59
60 use super::*;
61
62 fn temp_config(tag: &str, content: &str) -> PathBuf {
63 let dir = std::env::temp_dir().join(format!("muster-check-{tag}-{}", uuid::Uuid::new_v4()));
64 fs::create_dir_all(&dir).unwrap();
65 let path = dir.join("muster.yml");
66 fs::write(&path, content).unwrap();
67 path
68 }
69
70 #[test]
72 fn a_valid_config_passes() {
73 let path = temp_config("ok", "agents: []\nterminals: []\ncommands: []\n");
74 assert!(matches!(check(path), Ok(CheckOutcome::Valid)));
75 }
76
77 #[test]
79 fn broken_yaml_reports_the_error() {
80 let path = temp_config("bad", "agents: [unclosed\n");
81 match check(path) {
82 Ok(CheckOutcome::Invalid(report)) => assert!(!report.is_empty()),
83 other => panic!("must be a finding: {other:?}"),
84 }
85 }
86
87 #[test]
89 fn a_missing_file_is_a_command_failure() {
90 let path = std::env::temp_dir().join("muster-check-missing/muster.yml");
91 assert!(matches!(check(path), Err(CliError::Config(_))));
92 }
93
94 #[derive(Debug)]
96 struct EmbeddingError(WrappedError);
97
98 impl std::fmt::Display for EmbeddingError {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 write!(f, "could not parse: {}", self.0)
101 }
102 }
103
104 impl std::error::Error for EmbeddingError {
105 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
106 Some(&self.0)
107 }
108 }
109
110 #[derive(Debug)]
112 struct ContextError(WrappedError);
113
114 impl std::fmt::Display for ContextError {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 write!(f, "loading the config failed")
117 }
118 }
119
120 impl std::error::Error for ContextError {
121 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
122 Some(&self.0)
123 }
124 }
125
126 #[derive(Debug)]
127 struct WrappedError;
128
129 impl std::fmt::Display for WrappedError {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 write!(f, "unexpected token at line 3")
132 }
133 }
134
135 impl std::error::Error for WrappedError {}
136
137 #[test]
140 fn error_chain_skips_already_rendered_sources() {
141 assert_eq!(
142 error_chain(&EmbeddingError(WrappedError)),
143 "could not parse: unexpected token at line 3"
144 );
145 assert_eq!(
146 error_chain(&ContextError(WrappedError)),
147 "loading the config failed: unexpected token at line 3"
148 );
149 }
150}