use std::collections::{HashMap, HashSet};
use petgraph::{algo, graphmap::DiGraphMap, Direction};
use super::Request;
#[derive(Debug)]
pub struct RequestPlanner {
graph: DiGraphMap<usize, ()>,
order: Vec<usize>,
index_to_name: HashMap<usize, String>,
}
#[derive(Debug)]
pub enum PlanError {
DuplicateRequestName(String),
UnknownDependency { request: String, dependency: String },
MissingRequiredDependency { request: String, dependency: String },
CyclicDependency { cycle: Vec<String> },
InvalidSelection(usize),
}
impl RequestPlanner {
pub fn new(requests: &[Request]) -> Result<Self, PlanError> {
let mut name_to_index: HashMap<String, usize> = HashMap::new();
let mut index_to_name: HashMap<usize, String> = HashMap::new();
for (idx, request) in requests.iter().enumerate() {
if name_to_index
.insert(request.description.clone(), idx)
.is_some()
{
return Err(PlanError::DuplicateRequestName(request.description.clone()));
}
index_to_name.insert(idx, request.description.clone());
}
let mut graph: DiGraphMap<usize, ()> = DiGraphMap::new();
for (idx, request) in requests.iter().enumerate() {
graph.add_node(idx);
let declared: HashSet<&str> = request
.dependencies
.iter()
.map(|dep| dep.as_str())
.collect();
validate_capture_dependencies(request, &declared)?;
for dependency in &request.dependencies {
let dep_idx = name_to_index.get(dependency).copied().ok_or_else(|| {
PlanError::UnknownDependency {
request: request.description.clone(),
dependency: dependency.clone(),
}
})?;
graph.add_edge(dep_idx, idx, ());
}
}
let order = algo::toposort(&graph, None).map_err(|cycle| {
let start = cycle.node_id();
let mut stack = vec![start];
let mut visited = HashSet::new();
let mut cycle_names = Vec::new();
while let Some(node) = stack.pop() {
if !visited.insert(node) {
continue;
}
if let Some(name) = index_to_name.get(&node) {
cycle_names.push(name.clone());
}
for neighbor in graph.neighbors(node) {
if neighbor == start || !visited.contains(&neighbor) {
stack.push(neighbor);
}
}
}
PlanError::CyclicDependency { cycle: cycle_names }
})?;
Ok(Self {
graph,
order,
index_to_name,
})
}
pub fn order_all(&self) -> Vec<usize> {
self.order.clone()
}
pub fn order_for(&self, index: usize) -> Result<Vec<usize>, PlanError> {
if !self.index_to_name.contains_key(&index) {
return Err(PlanError::InvalidSelection(index));
}
let mut required: HashSet<usize> = HashSet::new();
self.collect_dependencies(index, &mut required);
required.insert(index);
let ordered = self
.order
.iter()
.copied()
.filter(|idx| required.contains(idx))
.collect();
Ok(ordered)
}
fn collect_dependencies(&self, node: usize, acc: &mut HashSet<usize>) {
for dep in self.graph.neighbors_directed(node, Direction::Incoming) {
if acc.insert(dep) {
self.collect_dependencies(dep, acc);
}
}
}
}
fn validate_capture_dependencies(
request: &Request,
declared: &HashSet<&str>,
) -> Result<(), PlanError> {
for capture in &request.response_captures {
if let Some(dep) = capture.required_dependency() {
if !declared.contains(dep) {
return Err(PlanError::MissingRequiredDependency {
request: request.description.clone(),
dependency: dep.to_string(),
});
}
}
}
Ok(())
}
impl std::fmt::Display for PlanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PlanError::DuplicateRequestName(name) => {
write!(f, "Multiple requests share the description '{}'.", name)
}
PlanError::UnknownDependency {
request,
dependency,
} => write!(
f,
"Request '{}' declares unknown dependency '{}'.",
request, dependency
),
PlanError::MissingRequiredDependency {
request,
dependency,
} => write!(
f,
"Request '{}' uses '&[{}]' but has not declared it with '> requires: {}'.",
request, dependency, dependency
),
PlanError::CyclicDependency { cycle } => {
if cycle.is_empty() {
write!(f, "Detected a circular dependency in the request graph.")
} else {
write!(
f,
"Detected a circular dependency involving: {}",
cycle.join(" -> ")
)
}
}
PlanError::InvalidSelection(index) => {
write!(f, "No request found at selection index {}.", index)
}
}
}
}
impl std::error::Error for PlanError {}
#[cfg(test)]
mod tests {
use super::*;
use http::Method;
use std::path::PathBuf;
fn empty_request(name: &str) -> Request {
Request {
description: name.to_string(),
method: Method::GET,
url: "https://example.com".to_string(),
headers: HashMap::new(),
query_params: HashMap::new(),
form_data: HashMap::new(),
body: None,
body_content_type: None,
callback_src: vec![],
response_captures: vec![],
assertions: vec![],
dependencies: vec![],
context: HashMap::new(),
working_dir: PathBuf::new(),
}
}
#[test]
fn orders_requests_in_dependency_sequence() {
let a = empty_request("A");
let mut b = empty_request("B");
let mut c = empty_request("C");
b.dependencies.push("A".to_string());
c.dependencies.push("B".to_string());
let requests = vec![a, b, c];
let planner = RequestPlanner::new(&requests).expect("planner should build");
assert_eq!(planner.order_all(), vec![0, 1, 2]);
}
#[test]
fn capture_requires_declared_dependency() {
let login = empty_request("Login");
let capture = super::super::response_capture::ResponseCapture::parse(
"&[Login].body.token > $TOKEN",
&HashMap::new(),
)
.expect("capture should parse");
let mut profile = empty_request("Profile");
profile.response_captures.push(capture);
let err = RequestPlanner::new(&[login, profile]).expect_err("planner should fail");
match err {
PlanError::MissingRequiredDependency {
request,
dependency,
} => {
assert_eq!(request, "Profile");
assert_eq!(dependency, "Login");
}
other => panic!("unexpected error: {:?}", other),
}
}
}