memstead_schema/
workspace_config.rs1use crate::config::ConfigError;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum CrossLinkValue {
23 Wildcard,
25 List(Vec<String>),
28}
29
30impl CrossLinkValue {
31 pub fn parse_toml(location: &str, value: &toml::Value) -> Result<Self, ConfigError> {
35 match value {
36 toml::Value::String(s) if s == "*" => Ok(Self::Wildcard),
37 toml::Value::String(other) => Err(ConfigError::Other(format!(
38 "{location}: expected `\"*\"` or a list of mem names, got string {other:?}"
39 ))),
40 toml::Value::Array(items) => {
41 let mut names: Vec<String> = Vec::with_capacity(items.len());
42 let mut has_wildcard = false;
43 for (idx, item) in items.iter().enumerate() {
44 match item {
45 toml::Value::String(s) if s == "*" => {
46 has_wildcard = true;
47 }
48 toml::Value::String(s) if s.is_empty() => {
49 return Err(ConfigError::Other(format!(
50 "{location}[{idx}]: mem name must not be empty"
51 )));
52 }
53 toml::Value::String(s) => names.push(s.clone()),
54 _ => {
55 return Err(ConfigError::Other(format!(
56 "{location}[{idx}]: expected string mem name, got {item}"
57 )));
58 }
59 }
60 }
61 if has_wildcard && !names.is_empty() {
62 return Err(ConfigError::Other(format!(
63 "{location}: `\"*\"` wildcard must be the sole entry — \
64 remove the named entries or drop the wildcard"
65 )));
66 }
67 if has_wildcard {
68 Ok(Self::Wildcard)
69 } else {
70 Ok(Self::List(names))
71 }
72 }
73 other => Err(ConfigError::Other(format!(
74 "{location}: expected `\"*\"` or a list of mem names, got {other}"
75 ))),
76 }
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 fn val(s: &str) -> toml::Value {
85 toml::from_str::<toml::Value>(&format!("x = {s}"))
86 .unwrap()
87 .get("x")
88 .unwrap()
89 .clone()
90 }
91
92 #[test]
93 fn wildcard_string_parses() {
94 assert_eq!(
95 CrossLinkValue::parse_toml("[loc]", &val("\"*\"")).unwrap(),
96 CrossLinkValue::Wildcard
97 );
98 }
99
100 #[test]
101 fn name_list_parses() {
102 assert_eq!(
103 CrossLinkValue::parse_toml("[loc]", &val("[\"a\", \"b\"]")).unwrap(),
104 CrossLinkValue::List(vec!["a".to_string(), "b".to_string()])
105 );
106 }
107
108 #[test]
109 fn empty_list_is_default_deny() {
110 assert_eq!(
111 CrossLinkValue::parse_toml("[loc]", &val("[]")).unwrap(),
112 CrossLinkValue::List(vec![])
113 );
114 }
115
116 #[test]
117 fn wildcard_in_a_list_is_a_lone_wildcard() {
118 assert_eq!(
119 CrossLinkValue::parse_toml("[loc]", &val("[\"*\"]")).unwrap(),
120 CrossLinkValue::Wildcard
121 );
122 }
123
124 #[test]
125 fn mixed_wildcard_and_names_is_rejected() {
126 let err = CrossLinkValue::parse_toml("[loc]", &val("[\"*\", \"a\"]")).unwrap_err();
127 assert!(format!("{err}").contains("sole entry"));
128 }
129
130 #[test]
131 fn empty_mem_name_is_rejected() {
132 let err = CrossLinkValue::parse_toml("[loc]", &val("[\"\"]")).unwrap_err();
133 assert!(format!("{err}").contains("must not be empty"));
134 }
135
136 #[test]
137 fn non_string_list_entry_is_rejected() {
138 let err = CrossLinkValue::parse_toml("[loc]", &val("[1]")).unwrap_err();
139 assert!(format!("{err}").contains("expected string mem name"));
140 }
141
142 #[test]
143 fn bare_integer_is_rejected() {
144 let err = CrossLinkValue::parse_toml("[loc]", &val("42")).unwrap_err();
145 assert!(format!("{err}").contains("expected"));
146 }
147}