feat: specify parameters configuration in recipe file (#2444)
This commit is contained in:
@@ -10,7 +10,7 @@ use crate::commands::mcp::run_server;
|
||||
use crate::commands::recipe::{handle_deeplink, handle_validate};
|
||||
use crate::commands::session::{handle_session_list, handle_session_remove};
|
||||
use crate::logging::setup_logging;
|
||||
use crate::recipe::load_recipe;
|
||||
use crate::recipes::recipe::load_recipe;
|
||||
use crate::session;
|
||||
use crate::session::{build_session, SessionBuilderConfig};
|
||||
use goose_bench::bench_config::BenchRunConfig;
|
||||
|
||||
@@ -2,7 +2,7 @@ use anyhow::Result;
|
||||
use base64::Engine;
|
||||
use console::style;
|
||||
|
||||
use crate::recipe::load_recipe;
|
||||
use crate::recipes::recipe::load_recipe;
|
||||
|
||||
/// Validates a recipe file
|
||||
///
|
||||
|
||||
@@ -3,7 +3,6 @@ use once_cell::sync::Lazy;
|
||||
pub mod cli;
|
||||
pub mod commands;
|
||||
pub mod logging;
|
||||
pub mod recipe;
|
||||
pub mod recipes;
|
||||
pub mod session;
|
||||
pub mod signal;
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use console::style;
|
||||
|
||||
use goose::recipe::Recipe;
|
||||
use minijinja::UndefinedBehavior;
|
||||
use serde_json::Value as JsonValue;
|
||||
use serde_yaml::Value as YamlValue;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::recipes::search_recipe::retrieve_recipe_file;
|
||||
|
||||
/// Loads and validates a recipe from a YAML or JSON file
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Path to the recipe file (YAML or JSON)
|
||||
/// * `log` - whether to log information about the recipe or not
|
||||
/// * `params` - optional parameters to render the recipe with
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The parsed recipe struct if successful
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - The file doesn't exist
|
||||
/// - The file can't be read
|
||||
/// - The YAML/JSON is invalid
|
||||
/// - The required fields are missing
|
||||
pub fn load_recipe(
|
||||
recipe_name: &str,
|
||||
log: bool,
|
||||
params: Option<Vec<(String, String)>>,
|
||||
) -> Result<Recipe> {
|
||||
let content = retrieve_recipe_file(recipe_name)?;
|
||||
|
||||
// Check if any parameters were provided
|
||||
let rendered_content = match params {
|
||||
None => content,
|
||||
Some(params) => render_content_with_params(&content, ¶ms)?,
|
||||
};
|
||||
|
||||
let recipe: Recipe;
|
||||
if serde_json::from_str::<JsonValue>(&rendered_content).is_ok() {
|
||||
recipe = serde_json::from_str(&rendered_content)?
|
||||
} else if serde_yaml::from_str::<YamlValue>(&rendered_content).is_ok() {
|
||||
recipe = serde_yaml::from_str(&rendered_content)?
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Unsupported file format for recipe file. Expected .yaml or .json"
|
||||
));
|
||||
}
|
||||
|
||||
if log {
|
||||
// Display information about the loaded recipe
|
||||
println!(
|
||||
"{} {}",
|
||||
style("Loading recipe:").green().bold(),
|
||||
style(&recipe.title).green()
|
||||
);
|
||||
println!("{} {}", style("Description:").dim(), &recipe.description);
|
||||
|
||||
println!(); // Add a blank line for spacing
|
||||
}
|
||||
|
||||
Ok(recipe)
|
||||
}
|
||||
|
||||
fn render_content_with_params(content: &str, params: &[(String, String)]) -> Result<String> {
|
||||
// Turn params into HashMap
|
||||
let param_map: HashMap<String, String> = params.iter().cloned().collect();
|
||||
|
||||
// Create a minijinja environment and context
|
||||
let mut env = minijinja::Environment::new();
|
||||
env.set_undefined_behavior(UndefinedBehavior::Strict);
|
||||
let template = env.template_from_str(content)
|
||||
.map_err(|_| anyhow::anyhow!("Failed to render recipe, please check if the recipe has proper syntax for variables: eg: {{ variable_name }}"))?;
|
||||
|
||||
// Render the template with the parameters
|
||||
template.render(param_map).map_err(|_| {
|
||||
anyhow::anyhow!(
|
||||
"Failed to render the recipe - please check if all required parameters are provided"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_render_content_with_params() {
|
||||
// Test basic parameter substitution
|
||||
let content = "Hello {{ name }}!";
|
||||
let params = vec![("name".to_string(), "World".to_string())];
|
||||
let result = render_content_with_params(content, ¶ms).unwrap();
|
||||
assert_eq!(result, "Hello World!");
|
||||
|
||||
// Test multiple parameters
|
||||
let content = "{{ greeting }} {{ name }}!";
|
||||
let params = vec![
|
||||
("greeting".to_string(), "Hi".to_string()),
|
||||
("name".to_string(), "Alice".to_string()),
|
||||
];
|
||||
let result = render_content_with_params(content, ¶ms).unwrap();
|
||||
assert_eq!(result, "Hi Alice!");
|
||||
|
||||
// Test missing parameter results in error
|
||||
let content = "Hello {{ missing }}!";
|
||||
let params = vec![];
|
||||
let err = render_content_with_params(content, ¶ms).unwrap_err();
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("please check if all required parameters"));
|
||||
|
||||
// Test invalid template syntax results in error
|
||||
let content = "Hello {{ unclosed";
|
||||
let params = vec![];
|
||||
let err = render_content_with_params(content, ¶ms).unwrap_err();
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("please check if the recipe has proper syntax"));
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod github_recipe;
|
||||
pub mod recipe;
|
||||
pub mod search_recipe;
|
||||
|
||||
@@ -0,0 +1,475 @@
|
||||
use anyhow::Result;
|
||||
use console::style;
|
||||
|
||||
use crate::recipes::search_recipe::retrieve_recipe_file;
|
||||
use goose::recipe::{Recipe, RecipeParameter, RecipeParameterRequirement};
|
||||
use minijinja::{Environment, Error, Template, UndefinedBehavior};
|
||||
use serde_json::Value as JsonValue;
|
||||
use serde_yaml::Value as YamlValue;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// Loads and validates a recipe from a YAML or JSON file
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Path to the recipe file (YAML or JSON)
|
||||
/// * `log` - whether to log information about the recipe or not
|
||||
/// * `params` - optional parameters to render the recipe with
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The parsed recipe struct if successful
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - The file doesn't exist
|
||||
/// - The file can't be read
|
||||
/// - The YAML/JSON is invalid
|
||||
/// - The required fields are missing
|
||||
pub fn load_recipe(
|
||||
recipe_name: &str,
|
||||
log: bool,
|
||||
params: Option<Vec<(String, String)>>,
|
||||
) -> Result<Recipe> {
|
||||
let recipe_file_content = retrieve_recipe_file(recipe_name)?;
|
||||
|
||||
let recipe_parameters = validate_recipe_file_parameters(&recipe_file_content)?;
|
||||
|
||||
let (rendered_content, params_for_template) = if let Some(user_params) = params {
|
||||
let params_for_template = apply_values_to_parameters(&user_params, recipe_parameters)?;
|
||||
(
|
||||
render_content_with_params(&recipe_file_content, ¶ms_for_template)?,
|
||||
Some(params_for_template),
|
||||
)
|
||||
} else {
|
||||
(recipe_file_content, None)
|
||||
};
|
||||
|
||||
let recipe = parse_recipe_content(&rendered_content)?;
|
||||
if log {
|
||||
// Display information about the loaded recipe
|
||||
println!(
|
||||
"{} {}",
|
||||
style("Loading recipe:").green().bold(),
|
||||
style(&recipe.title).green()
|
||||
);
|
||||
println!("{} {}", style("Description:").dim(), &recipe.description);
|
||||
|
||||
if let Some(params) = params_for_template {
|
||||
if !params.is_empty() {
|
||||
println!("{}", style("Parameters:").dim());
|
||||
for (key, value) in params {
|
||||
println!("{}: {}", key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!(); // Add a blank line for spacing
|
||||
}
|
||||
|
||||
Ok(recipe)
|
||||
}
|
||||
|
||||
fn validate_recipe_file_parameters(recipe_file_content: &str) -> Result<Vec<RecipeParameter>> {
|
||||
let recipe_from_recipe_file: Recipe = parse_recipe_content(recipe_file_content)?;
|
||||
validate_optional_parameters(&recipe_from_recipe_file)?;
|
||||
validate_parameters_in_template(recipe_from_recipe_file, recipe_file_content)
|
||||
}
|
||||
|
||||
fn validate_parameters_in_template(
|
||||
recipe: Recipe,
|
||||
recipe_file_content: &str,
|
||||
) -> Result<Vec<RecipeParameter>> {
|
||||
let template_variables = extract_template_variables(recipe_file_content)?;
|
||||
|
||||
let param_keys: HashSet<String> = recipe
|
||||
.parameters
|
||||
.as_ref()
|
||||
.unwrap_or(&vec![])
|
||||
.iter()
|
||||
.map(|p| p.key.clone())
|
||||
.collect();
|
||||
|
||||
let missing_keys = template_variables
|
||||
.difference(¶m_keys)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let extra_keys = param_keys
|
||||
.difference(&template_variables)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if missing_keys.is_empty() && extra_keys.is_empty() {
|
||||
return Ok(recipe.parameters.unwrap_or_default());
|
||||
}
|
||||
|
||||
let mut message = String::new();
|
||||
|
||||
if !missing_keys.is_empty() {
|
||||
message.push_str(&format!(
|
||||
"Missing definitions for parameters in the recipe file: {}.",
|
||||
missing_keys
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
}
|
||||
|
||||
if !extra_keys.is_empty() {
|
||||
message.push_str(&format!(
|
||||
"\nUnnecessary parameter definitions: {}.",
|
||||
extra_keys
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
}
|
||||
Err(anyhow::anyhow!("{}", message.trim_end()))
|
||||
}
|
||||
|
||||
fn validate_optional_parameters(recipe: &Recipe) -> Result<()> {
|
||||
let optional_params_without_default_values: Vec<String> = recipe
|
||||
.parameters
|
||||
.as_ref()
|
||||
.unwrap_or(&vec![])
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
matches!(p.requirement, RecipeParameterRequirement::Optional) && p.default.is_none()
|
||||
})
|
||||
.map(|p| p.key.clone())
|
||||
.collect();
|
||||
|
||||
if optional_params_without_default_values.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Optional parameters missing default values in the recipe: {}. Please provide defaults.", optional_params_without_default_values.join(", ")))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_recipe_content(content: &str) -> Result<Recipe> {
|
||||
if serde_json::from_str::<JsonValue>(content).is_ok() {
|
||||
Ok(serde_json::from_str(content)?)
|
||||
} else if serde_yaml::from_str::<YamlValue>(content).is_ok() {
|
||||
Ok(serde_yaml::from_str(content)?)
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"Unsupported file format for recipe file. Expected .yaml or .json"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_template_variables(template_str: &str) -> Result<HashSet<String>> {
|
||||
let mut env = Environment::new();
|
||||
env.set_undefined_behavior(UndefinedBehavior::Strict);
|
||||
|
||||
let template = env
|
||||
.template_from_str(template_str)
|
||||
.map_err(|e: Error| anyhow::anyhow!("Invalid template syntax: {}", e.to_string()))?;
|
||||
|
||||
Ok(template.undeclared_variables(true))
|
||||
}
|
||||
|
||||
fn apply_values_to_parameters(
|
||||
user_params: &[(String, String)],
|
||||
recipe_parameters: Vec<RecipeParameter>,
|
||||
) -> Result<HashMap<String, String>> {
|
||||
let mut param_map: HashMap<String, String> = user_params.iter().cloned().collect();
|
||||
let mut missing_params: Vec<String> = Vec::new();
|
||||
for param in recipe_parameters {
|
||||
if !param_map.contains_key(¶m.key) {
|
||||
match (¶m.default, ¶m.requirement) {
|
||||
(Some(default), _) => param_map.insert(param.key.clone(), default.clone()),
|
||||
(None, RecipeParameterRequirement::UserPrompt) => {
|
||||
let input_value = cliclack::input(format!(
|
||||
"Please enter {} ({})",
|
||||
param.key, param.description
|
||||
))
|
||||
.interact()?;
|
||||
param_map.insert(param.key.clone(), input_value)
|
||||
}
|
||||
_ => {
|
||||
missing_params.push(param.key.clone());
|
||||
None
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
match missing_params.is_empty() {
|
||||
true => Ok(param_map),
|
||||
false => {
|
||||
let formatted = missing_params
|
||||
.iter()
|
||||
.map(|key| format!("--params {}=your_value", key))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
Err(anyhow::anyhow!(
|
||||
"Please provide the following parameters in the command line: {}",
|
||||
formatted
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_content_with_params(content: &str, params: &HashMap<String, String>) -> Result<String> {
|
||||
// Create a minijinja environment and context
|
||||
let mut env = minijinja::Environment::new();
|
||||
env.set_undefined_behavior(UndefinedBehavior::Strict);
|
||||
let template: Template<'_, '_> = env.template_from_str(content)
|
||||
.map_err(|e: Error| anyhow::anyhow!("Failed to render recipe {}, please check if the recipe has proper syntax for variables: eg: {{ variable_name }}", e.to_string()))?;
|
||||
|
||||
// Render the template with the parameters
|
||||
template.render(params).map_err(|e: Error| {
|
||||
anyhow::anyhow!(
|
||||
"Failed to render the recipe {} - please check if all required parameters are provided",
|
||||
e.to_string()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use goose::recipe::{RecipeParameterInputType, RecipeParameterRequirement};
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn setup_recipe_file(instructions_and_parameters: &str) -> (TempDir, PathBuf) {
|
||||
let recipe_content = format!(
|
||||
r#"{{
|
||||
"version": "1.0.0",
|
||||
"title": "Test Recipe",
|
||||
"description": "A test recipe",
|
||||
{}
|
||||
}}"#,
|
||||
instructions_and_parameters
|
||||
);
|
||||
// Create a temporary file
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let recipe_path: std::path::PathBuf = temp_dir.path().join("test_recipe.json");
|
||||
std::fs::write(&recipe_path, recipe_content).unwrap();
|
||||
(temp_dir, recipe_path)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_content_with_params() {
|
||||
// Test basic parameter substitution
|
||||
let content = "Hello {{ name }}!";
|
||||
let mut params = HashMap::new();
|
||||
params.insert("name".to_string(), "World".to_string());
|
||||
let result = render_content_with_params(content, ¶ms).unwrap();
|
||||
assert_eq!(result, "Hello World!");
|
||||
|
||||
// Test multiple parameters
|
||||
let content = "{{ greeting }} {{ name }}!";
|
||||
let mut params = HashMap::new();
|
||||
params.insert("greeting".to_string(), "Hi".to_string());
|
||||
params.insert("name".to_string(), "Alice".to_string());
|
||||
let result = render_content_with_params(content, ¶ms).unwrap();
|
||||
assert_eq!(result, "Hi Alice!");
|
||||
|
||||
// Test missing parameter results in error
|
||||
let content = "Hello {{ missing }}!";
|
||||
let params = HashMap::new();
|
||||
let err = render_content_with_params(content, ¶ms).unwrap_err();
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("please check if all required parameters"));
|
||||
|
||||
// Test invalid template syntax results in error
|
||||
let content = "Hello {{ unclosed";
|
||||
let params = HashMap::new();
|
||||
let err = render_content_with_params(content, ¶ms).unwrap_err();
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("please check if the recipe has proper syntax"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_recipe_success() {
|
||||
let instructions_and_parameters = r#"
|
||||
"instructions": "Test instructions with {{ my_name }}",
|
||||
"parameters": [
|
||||
{
|
||||
"key": "my_name",
|
||||
"input_type": "string",
|
||||
"requirement": "required",
|
||||
"description": "A test parameter"
|
||||
}
|
||||
]"#;
|
||||
|
||||
let (_temp_dir, recipe_path) = setup_recipe_file(instructions_and_parameters);
|
||||
|
||||
let params = vec![("my_name".to_string(), "value".to_string())];
|
||||
let recipe = load_recipe(recipe_path.to_str().unwrap(), false, Some(params)).unwrap();
|
||||
|
||||
assert_eq!(recipe.title, "Test Recipe");
|
||||
assert_eq!(recipe.description, "A test recipe");
|
||||
assert_eq!(recipe.instructions.unwrap(), "Test instructions with value");
|
||||
// Verify parameters match recipe definition
|
||||
assert_eq!(recipe.parameters.as_ref().unwrap().len(), 1);
|
||||
let param = &recipe.parameters.as_ref().unwrap()[0];
|
||||
assert_eq!(param.key, "my_name");
|
||||
assert!(matches!(param.input_type, RecipeParameterInputType::String));
|
||||
assert!(matches!(
|
||||
param.requirement,
|
||||
RecipeParameterRequirement::Required
|
||||
));
|
||||
assert_eq!(param.description, "A test parameter");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_recipe_success_variable_in_prompt() {
|
||||
let instructions_and_parameters = r#"
|
||||
"instructions": "Test instructions",
|
||||
"prompt": "My prompt {{ my_name }}",
|
||||
"parameters": [
|
||||
{
|
||||
"key": "my_name",
|
||||
"input_type": "string",
|
||||
"requirement": "required",
|
||||
"description": "A test parameter"
|
||||
}
|
||||
]"#;
|
||||
|
||||
let (_temp_dir, recipe_path) = setup_recipe_file(instructions_and_parameters);
|
||||
|
||||
let params = vec![("my_name".to_string(), "value".to_string())];
|
||||
let recipe = load_recipe(recipe_path.to_str().unwrap(), false, Some(params)).unwrap();
|
||||
|
||||
assert_eq!(recipe.title, "Test Recipe");
|
||||
assert_eq!(recipe.description, "A test recipe");
|
||||
assert_eq!(recipe.instructions.unwrap(), "Test instructions");
|
||||
assert_eq!(recipe.prompt.unwrap(), "My prompt value");
|
||||
let param = &recipe.parameters.as_ref().unwrap()[0];
|
||||
assert_eq!(param.key, "my_name");
|
||||
assert!(matches!(param.input_type, RecipeParameterInputType::String));
|
||||
assert!(matches!(
|
||||
param.requirement,
|
||||
RecipeParameterRequirement::Required
|
||||
));
|
||||
assert_eq!(param.description, "A test parameter");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_recipe_wrong_parameters_in_recipe_file() {
|
||||
let instructions_and_parameters = r#"
|
||||
"instructions": "Test instructions with {{ expected_param1 }} {{ expected_param2 }}",
|
||||
"parameters": [
|
||||
{
|
||||
"key": "wrong_param_key",
|
||||
"input_type": "string",
|
||||
"requirement": "required",
|
||||
"description": "A test parameter"
|
||||
}
|
||||
]"#;
|
||||
let (_temp_dir, recipe_path) = setup_recipe_file(instructions_and_parameters);
|
||||
|
||||
let load_recipe_result = load_recipe(recipe_path.to_str().unwrap(), false, None);
|
||||
assert!(load_recipe_result.is_err());
|
||||
let err = load_recipe_result.unwrap_err();
|
||||
println!("{}", err.to_string());
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("Unnecessary parameter definitions: wrong_param_key."));
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("Missing definitions for parameters in the recipe file:"));
|
||||
assert!(err.to_string().contains("expected_param1"));
|
||||
assert!(err.to_string().contains("expected_param2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_recipe_with_default_values_in_recipe_file() {
|
||||
let instructions_and_parameters = r#"
|
||||
"instructions": "Test instructions with {{ param_with_default }} {{ param_without_default }}",
|
||||
"parameters": [
|
||||
{
|
||||
"key": "param_with_default",
|
||||
"input_type": "string",
|
||||
"requirement": "optional",
|
||||
"default": "my_default_value",
|
||||
"description": "A test parameter"
|
||||
},
|
||||
{
|
||||
"key": "param_without_default",
|
||||
"input_type": "string",
|
||||
"requirement": "required",
|
||||
"description": "A test parameter"
|
||||
}
|
||||
]"#;
|
||||
let (_temp_dir, recipe_path) = setup_recipe_file(instructions_and_parameters);
|
||||
let params = vec![("param_without_default".to_string(), "value1".to_string())];
|
||||
|
||||
let recipe = load_recipe(recipe_path.to_str().unwrap(), false, Some(params)).unwrap();
|
||||
|
||||
assert_eq!(recipe.title, "Test Recipe");
|
||||
assert_eq!(recipe.description, "A test recipe");
|
||||
assert_eq!(
|
||||
recipe.instructions.unwrap(),
|
||||
"Test instructions with my_default_value value1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_recipe_optional_parameters_without_default_values_in_recipe_file() {
|
||||
let instructions_and_parameters = r#"
|
||||
"instructions": "Test instructions with {{ optional_param }}",
|
||||
"parameters": [
|
||||
{
|
||||
"key": "optional_param",
|
||||
"input_type": "string",
|
||||
"requirement": "optional",
|
||||
"description": "A test parameter"
|
||||
}
|
||||
]"#;
|
||||
let (_temp_dir, recipe_path) = setup_recipe_file(instructions_and_parameters);
|
||||
|
||||
let load_recipe_result = load_recipe(recipe_path.to_str().unwrap(), false, None);
|
||||
assert!(load_recipe_result.is_err());
|
||||
let err = load_recipe_result.unwrap_err();
|
||||
println!("{}", err.to_string());
|
||||
assert!(err.to_string().contains(
|
||||
"Optional parameters missing default values in the recipe: optional_param. Please provide defaults."
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_recipe_wrong_input_type_in_recipe_file() {
|
||||
let instructions_and_parameters = r#"
|
||||
"instructions": "Test instructions with {{ param }}",
|
||||
"parameters": [
|
||||
{
|
||||
"key": "param",
|
||||
"input_type": "some_invalid_type",
|
||||
"requirement": "required",
|
||||
"description": "A test parameter"
|
||||
}
|
||||
]"#;
|
||||
let params = vec![("param".to_string(), "value".to_string())];
|
||||
let (_temp_dir, recipe_path) = setup_recipe_file(instructions_and_parameters);
|
||||
|
||||
let load_recipe_result = load_recipe(recipe_path.to_str().unwrap(), false, Some(params));
|
||||
assert!(load_recipe_result.is_err());
|
||||
let err = load_recipe_result.unwrap_err();
|
||||
assert!(err.to_string().contains("unknown variant `some_invalid_type`, expected one of `string`, `number`, `date`, `file`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_recipe_success_without_parameters() {
|
||||
let instructions_and_parameters = r#"
|
||||
"instructions": "Test instructions"
|
||||
"#;
|
||||
let (_temp_dir, recipe_path) = setup_recipe_file(instructions_and_parameters);
|
||||
|
||||
let load_recipe_result = load_recipe(recipe_path.to_str().unwrap(), false, None);
|
||||
assert!(load_recipe_result.is_ok());
|
||||
let recipe = load_recipe_result.unwrap();
|
||||
assert_eq!(recipe.instructions.unwrap(), "Test instructions");
|
||||
assert!(recipe.parameters.is_none());
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ fn default_version() -> String {
|
||||
/// * `context` - Supplementary context information for the Recipe
|
||||
/// * `activities` - Activity labels that appear when loading the Recipe
|
||||
/// * `author` - Information about the Recipe's creator and metadata
|
||||
/// * `parameters` - Additional parameters for the Recipe
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
@@ -47,6 +48,7 @@ fn default_version() -> String {
|
||||
/// context: None,
|
||||
/// activities: None,
|
||||
/// author: None,
|
||||
/// parameters: None,
|
||||
/// };
|
||||
/// ```
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@@ -78,6 +80,9 @@ pub struct Recipe {
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub author: Option<Author>, // any additional author information
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub parameters: Option<Vec<RecipeParameter>>, // any additional parameters for the recipe
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@@ -89,6 +94,33 @@ pub struct Author {
|
||||
pub metadata: Option<String>, // any additional metadata for the author
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RecipeParameterRequirement {
|
||||
Required,
|
||||
Optional,
|
||||
UserPrompt,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RecipeParameterInputType {
|
||||
String,
|
||||
Number,
|
||||
Date,
|
||||
File,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct RecipeParameter {
|
||||
pub key: String,
|
||||
pub input_type: RecipeParameterInputType,
|
||||
pub requirement: RecipeParameterRequirement,
|
||||
pub description: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default: Option<String>,
|
||||
}
|
||||
|
||||
/// Builder for creating Recipe instances
|
||||
pub struct RecipeBuilder {
|
||||
// Required fields with default values
|
||||
@@ -103,6 +135,7 @@ pub struct RecipeBuilder {
|
||||
context: Option<Vec<String>>,
|
||||
activities: Option<Vec<String>>,
|
||||
author: Option<Author>,
|
||||
parameters: Option<Vec<RecipeParameter>>,
|
||||
}
|
||||
|
||||
impl Recipe {
|
||||
@@ -131,6 +164,7 @@ impl Recipe {
|
||||
context: None,
|
||||
activities: None,
|
||||
author: None,
|
||||
parameters: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,6 +223,12 @@ impl RecipeBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the parameters for the Recipe
|
||||
pub fn parameters(mut self, parameters: Vec<RecipeParameter>) -> Self {
|
||||
self.parameters = Some(parameters);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds the Recipe instance
|
||||
///
|
||||
/// Returns an error if any required fields are missing
|
||||
@@ -210,6 +250,7 @@ impl RecipeBuilder {
|
||||
context: self.context,
|
||||
activities: self.activities,
|
||||
author: self.author,
|
||||
parameters: self.parameters,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user