feat: Import file contents from recipe 'file' input type parameter (#4558)
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
use crate::recipe::read_recipe_file_content::RecipeFile;
|
||||
use crate::recipe::read_recipe_file_content::{read_parameter_file_content, RecipeFile};
|
||||
use crate::recipe::template_recipe::{parse_recipe_content, render_recipe_content_with_params};
|
||||
use crate::recipe::{
|
||||
Recipe, RecipeParameter, RecipeParameterRequirement, BUILT_IN_RECIPE_DIR_PARAM,
|
||||
Recipe, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement,
|
||||
BUILT_IN_RECIPE_DIR_PARAM,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
@@ -146,9 +147,20 @@ fn validate_parameters_in_template(
|
||||
}
|
||||
|
||||
fn validate_optional_parameters(parameters: &Option<Vec<RecipeParameter>>) -> Result<()> {
|
||||
let optional_params_without_default_values: Vec<String> = parameters
|
||||
.as_ref()
|
||||
.unwrap_or(&vec![])
|
||||
let empty_params = vec![];
|
||||
let params = parameters.as_ref().unwrap_or(&empty_params);
|
||||
|
||||
let file_params_with_defaults: Vec<String> = params
|
||||
.iter()
|
||||
.filter(|p| matches!(p.input_type, RecipeParameterInputType::File) && p.default.is_some())
|
||||
.map(|p| p.key.clone())
|
||||
.collect();
|
||||
|
||||
if !file_params_with_defaults.is_empty() {
|
||||
return Err(anyhow::anyhow!("File parameters cannot have default values to avoid importing sensitive user files: {}", file_params_with_defaults.join(", ")));
|
||||
}
|
||||
|
||||
let optional_params_without_default_values: Vec<String> = params
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
matches!(p.requirement, RecipeParameterRequirement::Optional) && p.default.is_none()
|
||||
@@ -192,6 +204,10 @@ where
|
||||
None
|
||||
}
|
||||
};
|
||||
} else if matches!(param.input_type, RecipeParameterInputType::File) {
|
||||
let file_path = param_map.get(¶m.key).unwrap();
|
||||
let file_content = read_parameter_file_content(file_path)?;
|
||||
param_map.insert(param.key.clone(), file_content);
|
||||
}
|
||||
}
|
||||
Ok((param_map, missing_params))
|
||||
|
||||
@@ -33,6 +33,34 @@ mod tests {
|
||||
(temp_dir, recipe_file)
|
||||
}
|
||||
|
||||
fn setup_test_file(temp_dir: &TempDir, filename: &str, content: &str) -> std::path::PathBuf {
|
||||
let file_path = temp_dir.path().join(filename);
|
||||
std::fs::write(&file_path, content).unwrap();
|
||||
file_path
|
||||
}
|
||||
|
||||
fn setup_yaml_recipe_file(instructions_and_parameters: &str) -> (TempDir, RecipeFile) {
|
||||
let recipe_content = format!(
|
||||
r#"version: "1.0.0"
|
||||
title: "Test Recipe"
|
||||
description: "A test recipe"
|
||||
{}"#,
|
||||
instructions_and_parameters
|
||||
);
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let recipe_path = temp_dir.path().join("test_recipe.yaml");
|
||||
|
||||
std::fs::write(&recipe_path, recipe_content).unwrap();
|
||||
|
||||
let recipe_file = RecipeFile {
|
||||
content: std::fs::read_to_string(&recipe_path).unwrap(),
|
||||
parent_dir: temp_dir.path().to_path_buf(),
|
||||
file_path: recipe_path,
|
||||
};
|
||||
|
||||
(temp_dir, recipe_file)
|
||||
}
|
||||
|
||||
fn setup_yaml_recipe_files(
|
||||
parent_content: &str,
|
||||
child_content: &str,
|
||||
@@ -439,4 +467,86 @@ instructions: Child instructions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod file_parameter_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_build_recipe_file_parameter_valid_paths() {
|
||||
let instructions_and_parameters = r#"instructions: "Test file content: {{ FILE_PARAM }}"
|
||||
parameters:
|
||||
- key: FILE_PARAM
|
||||
input_type: file
|
||||
requirement: required
|
||||
description: A file parameter"#;
|
||||
|
||||
let (temp_dir, recipe_file) = setup_yaml_recipe_file(instructions_and_parameters);
|
||||
|
||||
let test_content = "Hello from file!\nThis is line 2\n Indented line 3";
|
||||
let test_file_path = setup_test_file(&temp_dir, "test_file.txt", test_content);
|
||||
|
||||
let params = vec![(
|
||||
"FILE_PARAM".to_string(),
|
||||
test_file_path.to_string_lossy().to_string(),
|
||||
)];
|
||||
let result = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let recipe = result.unwrap();
|
||||
|
||||
let instructions = recipe.instructions.as_ref().unwrap();
|
||||
assert!(instructions.contains("Hello from file!"));
|
||||
assert!(instructions.contains("Test file content:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_recipe_file_parameter_nonexistent_file() {
|
||||
let instructions_and_parameters = r#"instructions: "Test file content: {{ FILE_PARAM }}"
|
||||
parameters:
|
||||
- key: FILE_PARAM
|
||||
input_type: file
|
||||
requirement: required
|
||||
description: A file parameter"#;
|
||||
|
||||
let (_temp_dir, recipe_file) = setup_yaml_recipe_file(instructions_and_parameters);
|
||||
|
||||
let params = vec![(
|
||||
"FILE_PARAM".to_string(),
|
||||
"/nonexistent/path/file.txt".to_string(),
|
||||
)];
|
||||
let result = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT);
|
||||
|
||||
assert!(result.is_err());
|
||||
if let Err(RecipeError::TemplateRendering { source }) = result {
|
||||
assert!(source.to_string().contains("Failed to read parameter file"));
|
||||
} else {
|
||||
panic!("Expected TemplateRendering error");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_recipe_file_parameter_with_default_rejected() {
|
||||
let instructions_and_parameters = r#"instructions: "Test file content: {{ FILE_PARAM }}"
|
||||
parameters:
|
||||
- key: FILE_PARAM
|
||||
input_type: file
|
||||
requirement: required
|
||||
description: A file parameter
|
||||
default: "/etc/passwd""#;
|
||||
|
||||
let (_temp_dir, recipe_file) = setup_yaml_recipe_file(instructions_and_parameters);
|
||||
|
||||
let params = vec![];
|
||||
let result = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT);
|
||||
|
||||
assert!(result.is_err());
|
||||
if let Err(RecipeError::TemplateRendering { source }) = result {
|
||||
assert!(source
|
||||
.to_string()
|
||||
.contains("File parameters cannot have default values"));
|
||||
} else {
|
||||
panic!("Expected TemplateRendering error for file parameter with default");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +206,8 @@ pub enum RecipeParameterInputType {
|
||||
Number,
|
||||
Boolean,
|
||||
Date,
|
||||
/// File parameter that imports content from a file path.
|
||||
/// Cannot have default values to prevent importing sensitive user files.
|
||||
File,
|
||||
Select,
|
||||
}
|
||||
|
||||
@@ -58,3 +58,43 @@ fn convert_path_with_tilde_expansion(path: &Path) -> PathBuf {
|
||||
}
|
||||
PathBuf::from(path)
|
||||
}
|
||||
|
||||
pub fn read_parameter_file_content<P: AsRef<Path>>(file_path: P) -> Result<String> {
|
||||
let raw_path = file_path.as_ref();
|
||||
let path = convert_path_with_tilde_expansion(raw_path);
|
||||
|
||||
let content = fs::read_to_string(&path)
|
||||
.map_err(|e| anyhow!("Failed to read parameter file {}: {}", path.display(), e))?;
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_read_parameter_file_content_success() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let file_path = temp_dir.path().join("test_file.txt");
|
||||
let content = "Hello World\nSecond line\n Third line";
|
||||
std::fs::write(&file_path, content).unwrap();
|
||||
|
||||
let result = read_parameter_file_content(&file_path);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let expected = "Hello World\nSecond line\n Third line";
|
||||
assert_eq!(result.unwrap(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_parameter_file_content_nonexistent_file() {
|
||||
let result = read_parameter_file_content("/nonexistent/path/file.txt");
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Failed to read parameter file"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user