hen 0.16.0

Run protocol-aware API request collections from the command line or through MCP.
Documentation
use std::path::PathBuf;

use hen::{
    collection,
    error::{HenError, HenErrorKind, HenResult},
    prompt_generator::{collection_prompt, request_prompt, scan_directory},
    request,
};

use crate::cli::RunArgs;

pub(crate) fn load_collection(args: &RunArgs, cwd: PathBuf) -> HenResult<collection::Collection> {
    if args.non_interactive {
        return load_collection_non_interactive(args, cwd);
    }

    let collection = match args.path.as_ref() {
        Some(path) => {
            let path = PathBuf::from(path);
            if path.is_dir() {
                let hen_files = scan_directory(path.clone()).map_err(|err| {
                    err.with_detail(format!("While scanning directory {}", path.display()))
                })?;
                if hen_files.len() == 1 {
                    collection::Collection::with_environment(
                        hen_files[0].clone(),
                        args.env.as_deref(),
                    )
                } else {
                    collection_prompt(path)
                }
            } else {
                collection::Collection::with_environment(path, args.env.as_deref())
            }
        }
        None => collection_prompt(cwd),
    }?;

    Ok(collection)
}

fn load_collection_non_interactive(
    args: &RunArgs,
    cwd: PathBuf,
) -> HenResult<collection::Collection> {
    let path = args.path.as_ref().map(PathBuf::from).unwrap_or(cwd);

    if path.is_dir() {
        let hen_files = scan_directory(path.clone()).map_err(|err| {
            err.with_detail(format!("While scanning directory {}", path.display()))
        })?;

        return match hen_files.len() {
            0 => Err(
                HenError::new(HenErrorKind::Input, "No .hen files found in directory")
                    .with_detail(format!("Directory: {}", path.display()))
                    .with_exit_code(2),
            ),
            1 => collection::Collection::with_environment(
                hen_files[0].clone(),
                args.env.as_deref(),
            ),
            _ => Err(HenError::new(
                HenErrorKind::Input,
                "Directory contains multiple .hen files and cannot be resolved non-interactively",
            )
            .with_detail(format!("Directory: {}", path.display()))
            .with_detail("Provide a specific collection file path instead.")
            .with_exit_code(2)),
        };
    }

    collection::Collection::with_environment(path, args.env.as_deref())
}

pub(crate) fn resolve_execution_plan(
    collection: &collection::Collection,
    planner: &request::RequestPlanner,
    args: &RunArgs,
) -> HenResult<(Vec<usize>, Vec<usize>, Option<usize>)> {
    if args.non_interactive {
        return resolve_execution_plan_non_interactive(collection, planner, args.selector.as_deref());
    }

    match args.selector.as_deref() {
        Some("all") => {
            let order = planner.order_all();
            Ok((order.clone(), order, None))
        }
        Some(selector) => {
            let idx = selector.parse::<usize>().map_err(|_| {
                HenError::new(HenErrorKind::Input, "Selector must be an integer")
                    .with_detail(format!("Received: {}", selector))
                    .with_exit_code(2)
            })?;

            let order = planner.order_for(idx).map_err(|err| {
                HenError::new(HenErrorKind::Planner, "Failed to plan for selected request")
                    .with_detail(err.to_string())
            })?;

            Ok((order.clone(), vec![idx], Some(idx)))
        }
        None => {
            if collection.requests.len() == 1 {
                let order = planner.order_for(0).map_err(|err| {
                    HenError::new(HenErrorKind::Planner, "Failed to plan for the only request")
                        .with_detail(err.to_string())
                })?;

                Ok((order.clone(), vec![0], Some(0)))
            } else {
                let selection = request_prompt(collection);
                let order = planner.order_for(selection).map_err(|err| {
                    HenError::new(HenErrorKind::Planner, "Failed to plan for selected request")
                        .with_detail(err.to_string())
                })?;

                Ok((order.clone(), vec![selection], Some(selection)))
            }
        }
    }
}

fn resolve_execution_plan_non_interactive(
    collection: &collection::Collection,
    planner: &request::RequestPlanner,
    selector: Option<&str>,
) -> HenResult<(Vec<usize>, Vec<usize>, Option<usize>)> {
    match selector {
        Some("all") => {
            let order = planner.order_all();
            Ok((order.clone(), order, None))
        }
        Some(selector) => {
            let idx = selector.parse::<usize>().map_err(|_| {
                HenError::new(HenErrorKind::Input, "Selector must be an integer or 'all'")
                    .with_detail(format!("Received: {}", selector))
                    .with_exit_code(2)
            })?;

            let order = planner.order_for(idx).map_err(|err| {
                HenError::new(HenErrorKind::Planner, "Failed to plan for selected request")
                    .with_detail(err.to_string())
            })?;

            Ok((order.clone(), vec![idx], Some(idx)))
        }
        None => {
            if collection.requests.len() == 1 {
                let order = planner.order_for(0).map_err(|err| {
                    HenError::new(HenErrorKind::Planner, "Failed to plan for the only request")
                        .with_detail(err.to_string())
                })?;

                Ok((order.clone(), vec![0], Some(0)))
            } else {
                Err(HenError::new(
                    HenErrorKind::Input,
                    "A selector is required when a collection contains multiple requests",
                )
                .with_detail("Provide a request index or use 'all'.")
                .with_exit_code(2))
            }
        }
    }
}