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]);
}
}