1use crate::Recipe;
2use snafu::prelude::*;
3
4#[derive(Debug, Clone, PartialEq)]
5pub struct ValidationResult {
6 pub is_valid: bool,
7 pub errors: Vec<ValidationError>,
8}
9
10#[derive(Debug, Clone, PartialEq)]
11pub struct ValidationError {
12 pub parameter: String,
13 pub message: String,
14}
15
16#[derive(Debug, Clone, PartialEq)]
17pub struct SignatureHelp {
18 pub recipe_name: String,
19 pub parameters: Vec<ParameterInfo>,
20 pub documentation: Option<String>,
21}
22
23#[derive(Debug, Clone, PartialEq)]
24pub struct ParameterInfo {
25 pub name: String,
26 pub required: bool,
27 pub default_value: Option<String>,
28 pub description: Option<String>,
29}
30
31#[derive(Debug, Snafu)]
32pub enum ValidationSnafu {
33 #[snafu(display("Recipe '{}' not found", recipe_name))]
34 RecipeNotFound { recipe_name: String },
35}
36
37pub type Result<T> = std::result::Result<T, ValidationSnafu>;
38
39pub fn validate_arguments(recipe: &Recipe, args: &[String]) -> ValidationResult {
41 let mut errors = Vec::new();
42 let params = &recipe.parameters;
43
44 if args.len() > params.len() {
46 errors.push(ValidationError {
47 parameter: "<extra>".to_string(),
48 message: format!(
49 "Too many arguments: expected at most {}, got {}",
50 params.len(),
51 args.len()
52 ),
53 });
54 }
55
56 for (i, param) in params.iter().enumerate() {
58 if i >= args.len() {
59 if param.default_value.is_none() {
61 errors.push(ValidationError {
62 parameter: param.name.clone(),
63 message: format!("Missing required parameter: {}", param.name),
64 });
65 }
66 }
67 }
69
70 ValidationResult {
71 is_valid: errors.is_empty(),
72 errors,
73 }
74}
75
76pub fn get_signature_help(recipe: &Recipe) -> SignatureHelp {
78 let parameters = recipe
79 .parameters
80 .iter()
81 .map(|param| ParameterInfo {
82 name: param.name.clone(),
83 required: param.default_value.is_none(),
84 default_value: param.default_value.clone(),
85 description: None, })
87 .collect();
88
89 SignatureHelp {
90 recipe_name: recipe.name.clone(),
91 parameters,
92 documentation: recipe.documentation.clone(),
93 }
94}
95
96pub fn format_signature_help(help: &SignatureHelp) -> String {
98 let mut result = String::new();
99
100 result.push_str(&format!("{}(", help.recipe_name));
102
103 let param_strings: Vec<String> = help
104 .parameters
105 .iter()
106 .map(|param| {
107 if param.required {
108 param.name.clone()
109 } else {
110 format!(
111 "{}={}",
112 param.name,
113 param.default_value.as_deref().unwrap_or("")
114 )
115 }
116 })
117 .collect();
118
119 result.push_str(¶m_strings.join(", "));
120 result.push(')');
121
122 let has_doc = help.documentation.is_some();
124 let has_params = !help.parameters.is_empty();
125
126 if let Some(ref doc) = help.documentation {
128 result.push_str(&format!("\n\n{doc}"));
129 }
130
131 if has_params {
133 if has_doc {
134 result.push_str("\n\nParameters:");
135 } else {
136 result.push_str("\nParameters:");
137 }
138 for param in &help.parameters {
139 result.push_str(&format!("\n {}", param.name));
140 if param.required {
141 result.push_str(" (required)");
142 } else {
143 let default_display = match param.default_value.as_deref() {
144 Some("") => "none",
145 Some(val) => val,
146 None => "none",
147 };
148 result.push_str(&format!(" (optional, default: {default_display})"));
149 }
150 if let Some(ref desc) = param.description {
151 result.push_str(&format!(" - {desc}"));
152 }
153 }
154 }
155
156 result
157}
158
159pub fn validate_with_help(recipe: &Recipe, args: &[String]) -> ValidationResult {
161 let mut result = validate_arguments(recipe, args);
162
163 if !result.is_valid {
165 let help = get_signature_help(recipe);
166 let formatted_help = format_signature_help(&help);
167
168 if let Some(first_error) = result.errors.first_mut() {
170 first_error.message = format!(
171 "{}\n\nExpected signature:\n{}",
172 first_error.message, formatted_help
173 );
174 }
175 }
176
177 result
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use crate::Parameter;
184
185 fn create_test_recipe(name: &str, params: Vec<Parameter>) -> Recipe {
186 Recipe {
187 name: name.to_string(),
188 parameters: params,
189 documentation: Some(format!("Test recipe {}", name)),
190 body: String::new(),
191 dependencies: Vec::new(),
192 }
193 }
194
195 #[test]
196 fn test_validate_arguments_success() {
197 let params = vec![
198 Parameter {
199 name: "env".to_string(),
200 default_value: None,
201 },
202 Parameter {
203 name: "target".to_string(),
204 default_value: Some("prod".to_string()),
205 },
206 ];
207 let recipe = create_test_recipe("deploy", params);
208
209 let result = validate_arguments(&recipe, &["staging".to_string()]);
211 assert!(result.is_valid);
212 assert!(result.errors.is_empty());
213
214 let result = validate_arguments(&recipe, &["staging".to_string(), "dev".to_string()]);
216 assert!(result.is_valid);
217 assert!(result.errors.is_empty());
218 }
219
220 #[test]
221 fn test_validate_arguments_missing_required() {
222 let params = vec![
223 Parameter {
224 name: "env".to_string(),
225 default_value: None,
226 },
227 Parameter {
228 name: "target".to_string(),
229 default_value: Some("prod".to_string()),
230 },
231 ];
232 let recipe = create_test_recipe("deploy", params);
233
234 let result = validate_arguments(&recipe, &[]);
235 assert!(!result.is_valid);
236 assert_eq!(result.errors.len(), 1);
237 assert_eq!(result.errors[0].parameter, "env");
238 assert!(
239 result.errors[0]
240 .message
241 .contains("Missing required parameter")
242 );
243 }
244
245 #[test]
246 fn test_validate_arguments_too_many() {
247 let params = vec![Parameter {
248 name: "env".to_string(),
249 default_value: None,
250 }];
251 let recipe = create_test_recipe("deploy", params);
252
253 let result = validate_arguments(&recipe, &["staging".to_string(), "extra".to_string()]);
254 assert!(!result.is_valid);
255 assert_eq!(result.errors.len(), 1);
256 assert_eq!(result.errors[0].parameter, "<extra>");
257 assert!(result.errors[0].message.contains("Too many arguments"));
258 }
259
260 #[test]
261 fn test_validate_arguments_no_parameters() {
262 let recipe = create_test_recipe("build", vec![]);
263
264 let result = validate_arguments(&recipe, &[]);
266 assert!(result.is_valid);
267
268 let result = validate_arguments(&recipe, &["unexpected".to_string()]);
270 assert!(!result.is_valid);
271 }
272
273 #[test]
274 fn test_get_signature_help() {
275 let params = vec![
276 Parameter {
277 name: "env".to_string(),
278 default_value: None,
279 },
280 Parameter {
281 name: "target".to_string(),
282 default_value: Some("prod".to_string()),
283 },
284 Parameter {
285 name: "verbose".to_string(),
286 default_value: Some("false".to_string()),
287 },
288 ];
289 let recipe = create_test_recipe("deploy", params);
290
291 let help = get_signature_help(&recipe);
292
293 assert_eq!(help.recipe_name, "deploy");
294 assert_eq!(help.parameters.len(), 3);
295 assert_eq!(help.documentation, Some("Test recipe deploy".to_string()));
296
297 assert_eq!(help.parameters[0].name, "env");
299 assert!(help.parameters[0].required);
300 assert_eq!(help.parameters[0].default_value, None);
301
302 assert_eq!(help.parameters[1].name, "target");
303 assert!(!help.parameters[1].required);
304 assert_eq!(help.parameters[1].default_value, Some("prod".to_string()));
305
306 assert_eq!(help.parameters[2].name, "verbose");
307 assert!(!help.parameters[2].required);
308 assert_eq!(help.parameters[2].default_value, Some("false".to_string()));
309 }
310
311 #[test]
312 fn test_format_signature_help() {
313 let params = vec![
314 Parameter {
315 name: "env".to_string(),
316 default_value: None,
317 },
318 Parameter {
319 name: "target".to_string(),
320 default_value: Some("prod".to_string()),
321 },
322 ];
323 let recipe = create_test_recipe("deploy", params);
324 let help = get_signature_help(&recipe);
325
326 let formatted = format_signature_help(&help);
327
328 assert!(formatted.contains("deploy(env, target=prod)"));
329 assert!(formatted.contains("Test recipe deploy"));
330 assert!(formatted.contains("env (required)"));
331 assert!(formatted.contains("target (optional, default: prod)"));
332 }
333
334 #[test]
335 fn test_validate_with_help() {
336 let params = vec![Parameter {
337 name: "env".to_string(),
338 default_value: None,
339 }];
340 let recipe = create_test_recipe("deploy", params);
341
342 let result = validate_with_help(&recipe, &[]);
343 assert!(!result.is_valid);
344 assert_eq!(result.errors.len(), 1);
345 assert!(
346 result.errors[0]
347 .message
348 .contains("Missing required parameter")
349 );
350 assert!(result.errors[0].message.contains("Expected signature"));
351 assert!(result.errors[0].message.contains("deploy(env)"));
352 }
353}