Applied server side call to parse and save recipe (#5022)

This commit is contained in:
Lifei Zhou
2025-10-09 15:45:46 +11:00
committed by GitHub
parent fc7836d649
commit 1396d315e5
42 changed files with 868 additions and 2126 deletions
+1 -1
View File
@@ -1019,7 +1019,7 @@ pub async fn cli() -> Result<()> {
.and_then(|rf| {
goose::recipe::template_recipe::parse_recipe_content(
&rf.content,
rf.parent_dir.to_string_lossy().to_string(),
Some(rf.parent_dir.to_string_lossy().to_string()),
)
.ok()
.map(|(r, _)| r.version)
+14 -90
View File
@@ -1,43 +1,25 @@
use anyhow::Result;
use console::style;
use goose::recipe::validate_recipe::validate_recipe_template_from_file;
use crate::recipes::github_recipe::RecipeSource;
use crate::recipes::recipe::load_recipe_for_validation;
use crate::recipes::search_recipe::list_available_recipes;
use crate::recipes::search_recipe::{list_available_recipes, load_recipe_file};
use goose::recipe_deeplink;
/// Validates a recipe file
///
/// # Arguments
///
/// * `file_path` - Path to the recipe file to validate
///
/// # Returns
///
/// Result indicating success or failure
pub fn handle_validate(recipe_name: &str) -> Result<()> {
// Load and validate the recipe file
match load_recipe_for_validation(recipe_name) {
Ok(_) => {
println!("{} recipe file is valid", style("").green().bold());
Ok(())
}
Err(err) => {
println!("{} {}", style("").red().bold(), err);
Err(err)
}
}
let recipe_file = load_recipe_file(recipe_name)?;
validate_recipe_template_from_file(&recipe_file).map_err(|err| {
anyhow::anyhow!(
"{} recipe file is invalid: {}",
style("").red().bold(),
err
)
})?;
println!("{} recipe file is valid", style("").green().bold());
Ok(())
}
/// Generates a deeplink for a recipe file
///
/// # Arguments
///
/// * `recipe_name` - Path to the recipe file
///
/// # Returns
///
/// Result indicating success or failure
pub fn handle_deeplink(recipe_name: &str) -> Result<String> {
match generate_deeplink(recipe_name) {
Ok((deeplink_url, recipe)) => {
@@ -60,15 +42,6 @@ pub fn handle_deeplink(recipe_name: &str) -> Result<String> {
}
}
/// Opens a recipe in Goose Desktop
///
/// # Arguments
///
/// * `recipe_name` - Path to the recipe file
///
/// # Returns
///
/// Result indicating success or failure
pub fn handle_open(recipe_name: &str) -> Result<()> {
// Generate the deeplink using the helper function (no printing)
// This reuses all the validation and encoding logic
@@ -107,16 +80,6 @@ pub fn handle_open(recipe_name: &str) -> Result<()> {
}
}
/// Lists all available recipes from local paths and GitHub repositories
///
/// # Arguments
///
/// * `format` - Output format ("text" or "json")
/// * `verbose` - Whether to show detailed information
///
/// # Returns
///
/// Result indicating success or failure
pub fn handle_list(format: &str, verbose: bool) -> Result<()> {
let recipes = match list_available_recipes() {
Ok(recipes) => recipes,
@@ -168,18 +131,10 @@ pub fn handle_list(format: &str, verbose: bool) -> Result<()> {
Ok(())
}
/// Helper function to generate a deeplink
///
/// # Arguments
///
/// * `recipe_name` - Path to the recipe file
///
/// # Returns
///
/// Result containing the deeplink URL and recipe
fn generate_deeplink(recipe_name: &str) -> Result<(String, goose::recipe::Recipe)> {
let recipe_file = load_recipe_file(recipe_name)?;
// Load the recipe file first to validate it
let recipe = load_recipe_for_validation(recipe_name)?;
let recipe = validate_recipe_template_from_file(&recipe_file)?;
match recipe_deeplink::encode(&recipe) {
Ok(encoded) => {
let full_url = format!("goose://recipe?config={}", encoded);
@@ -227,20 +182,6 @@ prompt: "Test prompt content {{ name }}"
instructions: "Test instructions"
"#;
const RECIPE_WITH_INVALID_JSON_SCHEMA: &str = r#"
title: "Test Recipe with Invalid JSON Schema"
description: "A test recipe with invalid JSON schema"
prompt: "Test prompt content"
instructions: "Test instructions"
response:
json_schema:
type: invalid_type
properties:
result:
type: unknown_type
required: "should_be_array_not_string"
"#;
#[test]
fn test_handle_deeplink_valid_recipe() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
@@ -305,23 +246,6 @@ response:
assert!(result.is_err());
}
#[test]
fn test_handle_validation_recipe_with_invalid_json_schema() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let recipe_path = create_test_recipe_file(
&temp_dir,
"test_recipe.yaml",
RECIPE_WITH_INVALID_JSON_SCHEMA,
);
let result = handle_validate(&recipe_path);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("JSON schema validation failed"));
}
#[test]
fn test_generate_deeplink_valid_recipe() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
@@ -331,7 +331,7 @@ fn get_github_recipe_info(repo: &str, dir_name: &str, recipe_filename: &str) ->
.map_err(|e| anyhow!("Failed to convert content to string: {}", e))?;
// Parse the recipe content
let (recipe, _) = parse_recipe_content(&content, format!("{}/{}", repo, dir_name))?;
let (recipe, _) = parse_recipe_content(&content, Some(format!("{}/{}", repo, dir_name)))?;
return Ok(RecipeInfo {
name: dir_name.to_string(),
+5 -30
View File
@@ -7,13 +7,13 @@ use crate::recipes::secret_discovery::{discover_recipe_secrets, SecretRequiremen
use anyhow::Result;
use goose::config::Config;
use goose::recipe::build_recipe::{
apply_values_to_parameters, build_recipe_from_template, validate_recipe_parameters, RecipeError,
apply_values_to_parameters, build_recipe_from_template, RecipeError,
};
use goose::recipe::read_recipe_file_content::RecipeFile;
use goose::recipe::template_recipe::render_recipe_for_preview;
use goose::recipe::validate_recipe::validate_recipe_parameters;
use goose::recipe::Recipe;
use serde_json::Value;
use std::collections::HashMap;
fn create_user_prompt_callback() -> impl Fn(&str, &str) -> Result<String> {
|key: &str, description: &str| -> Result<String> {
@@ -131,29 +131,11 @@ pub fn render_recipe_as_yaml(recipe_name: &str, params: Vec<(String, String)>) -
}
}
pub fn load_recipe_for_validation(recipe_name: &str) -> Result<Recipe> {
let (recipe_file, recipe_dir_str) = load_recipe_file_with_dir(recipe_name)?;
let recipe_file_content = &recipe_file.content;
validate_recipe_parameters(recipe_file_content, &recipe_dir_str)?;
let recipe = render_recipe_for_preview(
recipe_file_content,
recipe_dir_str.to_string(),
&HashMap::new(),
)?;
if let Some(response) = &recipe.response {
if let Some(json_schema) = &response.json_schema {
validate_json_schema(json_schema)?;
}
}
Ok(recipe)
}
pub fn explain_recipe(recipe_name: &str, params: Vec<(String, String)>) -> Result<()> {
let (recipe_file, recipe_dir_str) = load_recipe_file_with_dir(recipe_name)?;
let recipe_file_content = &recipe_file.content;
let recipe_parameters = validate_recipe_parameters(recipe_file_content, &recipe_dir_str)?;
let recipe_parameters =
validate_recipe_parameters(recipe_file_content, Some(recipe_dir_str.clone()))?;
let (params_for_template, missing_params) = apply_values_to_parameters(
&params,
@@ -163,7 +145,7 @@ pub fn explain_recipe(recipe_name: &str, params: Vec<(String, String)>) -> Resul
)?;
let recipe = render_recipe_for_preview(
recipe_file_content,
recipe_dir_str.to_string(),
Some(recipe_dir_str.clone()),
&params_for_template,
)?;
print_recipe_explanation(&recipe);
@@ -172,13 +154,6 @@ pub fn explain_recipe(recipe_name: &str, params: Vec<(String, String)>) -> Resul
Ok(())
}
fn validate_json_schema(schema: &serde_json::Value) -> Result<()> {
match jsonschema::validator_for(schema) {
Ok(_) => Ok(()),
Err(err) => Err(anyhow::anyhow!("JSON schema validation failed: {}", err)),
}
}
#[cfg(test)]
mod tests {
use goose::recipe::{RecipeParameterInputType, RecipeParameterRequirement};