hen 0.20.1

Run protocol-aware API request collections from the command line or through MCP.
Documentation
use crate::{
    contract::ContractOperation,
    error::{HenError, HenErrorKind, HenResult},
};

pub fn resolve_selected_operations(
    operations: &[ContractOperation],
    selector: Option<&str>,
) -> HenResult<Vec<usize>> {
    if operations.is_empty() {
        return Err(
            HenError::new(HenErrorKind::Input, "OpenAPI spec contains no importable operations")
                .with_exit_code(2),
        );
    }

    let selected = match selector {
        None | Some("all") => (0..operations.len()).collect::<Vec<_>>(),
        Some(selector) if selector.starts_with("tag:") => {
            let tag = selector.trim_start_matches("tag:").trim();
            operations
                .iter()
                .enumerate()
                .filter_map(|(index, operation)| operation.tags.iter().any(|value| value == tag).then_some(index))
                .collect::<Vec<_>>()
        }
        Some(selector) => {
            if let Ok(index) = selector.parse::<usize>() {
                if index >= operations.len() {
                    return Err(
                        HenError::new(HenErrorKind::Input, "Selector index is out of range")
                            .with_detail(format!("Selector: {index}"))
                            .with_detail(format!("Importable operations: {}", operations.len()))
                            .with_exit_code(2),
                    );
                }
                vec![index]
            } else if let Some((method, path)) = parse_method_path(selector) {
                operations
                    .iter()
                    .enumerate()
                    .filter_map(|(index, operation)| {
                        (operation.method == method && operation.path == path).then_some(index)
                    })
                    .collect::<Vec<_>>()
            } else {
                operations
                    .iter()
                    .enumerate()
                    .filter_map(|(index, operation)| {
                        (operation.operation_id.as_deref() == Some(selector)).then_some(index)
                    })
                    .collect::<Vec<_>>()
            }
        }
    };

    if selected.is_empty() {
        return Err(
            HenError::new(HenErrorKind::Input, "Selector matched no OpenAPI operations")
                .with_detail(match selector {
                    Some(selector) => format!("Selector: {selector}"),
                    None => "Selector: <none>".to_string(),
                })
                .with_exit_code(2),
        );
    }

    Ok(selected)
}

fn parse_method_path(selector: &str) -> Option<(String, String)> {
    let mut parts = selector.splitn(2, char::is_whitespace);
    let method = parts.next()?.trim();
    let path = parts.next()?.trim();
    if method.is_empty() || path.is_empty() {
        return None;
    }
    Some((method.to_uppercase(), path.to_string()))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::contract::ContractParameter;

    fn operation(operation_id: Option<&str>, method: &str, path: &str, tags: &[&str]) -> ContractOperation {
        ContractOperation {
            operation_id: operation_id.map(ToOwned::to_owned),
            summary: None,
            method: method.to_string(),
            path: path.to_string(),
            servers: Vec::new(),
            tags: tags.iter().map(|value| value.to_string()).collect(),
            parameters: Vec::<ContractParameter>::new(),
            request_body: None,
            response_body: None,
            security: Vec::new(),
            source_span: None,
        }
    }

    #[test]
    fn omitting_selector_selects_all_operations() {
        let operations = vec![operation(Some("listPets"), "GET", "/pets", &["pets"]), operation(Some("createPet"), "POST", "/pets", &["pets"])];
        let selected = resolve_selected_operations(&operations, None).expect("selection should resolve");
        assert_eq!(selected, vec![0, 1]);
    }

    #[test]
    fn resolves_operation_id_selector() {
        let operations = vec![operation(Some("listPets"), "GET", "/pets", &["pets"]), operation(Some("createPet"), "POST", "/pets", &["pets"])];
        let selected = resolve_selected_operations(&operations, Some("createPet")).expect("selection should resolve");
        assert_eq!(selected, vec![1]);
    }

    #[test]
    fn resolves_method_path_selector() {
        let operations = vec![operation(Some("listPets"), "GET", "/pets", &["pets"]), operation(Some("createPet"), "POST", "/pets", &["pets"])];
        let selected = resolve_selected_operations(&operations, Some("POST /pets")).expect("selection should resolve");
        assert_eq!(selected, vec![1]);
    }

    #[test]
    fn resolves_tag_selector() {
        let operations = vec![operation(Some("listPets"), "GET", "/pets", &["pets"]), operation(Some("listUsers"), "GET", "/users", &["users"]), operation(Some("createPet"), "POST", "/pets", &["pets"])];
        let selected = resolve_selected_operations(&operations, Some("tag:pets")).expect("selection should resolve");
        assert_eq!(selected, vec![0, 2]);
    }
}