feat: Import file contents from recipe 'file' input type parameter (#4558)

This commit is contained in:
Jarrod Sibbison
2025-09-10 21:13:56 +10:00
committed by GitHub
parent c85373e944
commit 85fb6d8f23
4 changed files with 173 additions and 5 deletions
+21 -5
View File
@@ -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(&param.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");
}
}
}
}
+2
View File
@@ -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"));
}
}