use axum::http::StatusCode;
use axum::response::Response;
use knot::models::RepoScope;
use crate::handlers::models::error_response;
use crate::models::AppState;
pub(crate) const DEFAULT_MAX_RESULTS: usize = 5;
pub(crate) const MIN_MAX_RESULTS: usize = 1;
pub(crate) const MAX_MAX_RESULTS: usize = 100;
pub fn resolve_scope(raw: Option<&str>, known: &[String]) -> Result<RepoScope, Vec<String>> {
let scope = RepoScope::parse_optional(raw);
if matches!(scope, RepoScope::All) {
return Ok(scope);
}
let mut unknown: Vec<String> = scope
.filter_names()
.into_iter()
.filter(|name| !known.contains(name))
.collect();
unknown.sort();
unknown.dedup();
if unknown.is_empty() {
Ok(scope)
} else {
Err(unknown)
}
}
pub fn clamp_max_results(requested: Option<usize>) -> usize {
requested
.unwrap_or(DEFAULT_MAX_RESULTS)
.clamp(MIN_MAX_RESULTS, MAX_MAX_RESULTS)
}
pub(crate) fn scope_or_error(
state: &AppState,
raw: Option<&str>,
) -> Result<RepoScope, Vec<String>> {
let known: Vec<String> = {
let mut registry = state.registry.lock().unwrap();
registry.list().iter().map(|r| r.id.clone()).collect()
};
resolve_scope(raw, &known)
}
pub(crate) fn unknown_repos_error(unknown: &[String]) -> Response {
error_response(
StatusCode::BAD_REQUEST,
format!("Unknown repository ids: {}", unknown.join(", ")),
)
}
pub(crate) fn scope_fields(scope: &RepoScope) -> (&'static str, usize) {
match scope {
RepoScope::All => ("all", 0),
RepoScope::One(_) => ("one", 1),
RepoScope::Many(names) => ("many", names.len()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn known() -> Vec<String> {
vec!["repo-a".to_string(), "repo-b".to_string()]
}
#[test]
fn absent_repo_param_is_all() {
assert_eq!(resolve_scope(None, &known()), Ok(RepoScope::All));
}
#[test]
fn empty_repo_param_is_all() {
assert_eq!(resolve_scope(Some(""), &known()), Ok(RepoScope::All));
assert_eq!(resolve_scope(Some(" "), &known()), Ok(RepoScope::All));
}
#[test]
fn sentinel_all_is_case_insensitive() {
for raw in ["all", "ALL", "All", "*"] {
assert_eq!(
resolve_scope(Some(raw), &known()),
Ok(RepoScope::All),
"sentinel '{raw}' must resolve to All"
);
}
}
#[test]
fn single_known_repo_is_one() {
assert_eq!(
resolve_scope(Some("repo-a"), &known()),
Ok(RepoScope::One("repo-a".to_string()))
);
}
#[test]
fn list_of_known_repos_is_many() {
assert_eq!(
resolve_scope(Some("repo-a,repo-b"), &known()),
Ok(RepoScope::Many(vec![
"repo-a".to_string(),
"repo-b".to_string()
]))
);
}
#[test]
fn whitespace_and_duplicates_are_normalised() {
assert_eq!(
resolve_scope(Some(" repo-a , repo-a , repo-b "), &known()),
Ok(RepoScope::Many(vec![
"repo-a".to_string(),
"repo-b".to_string()
]))
);
}
#[test]
fn unknown_single_name_is_rejected() {
assert_eq!(
resolve_scope(Some("ghost"), &known()),
Err(vec!["ghost".to_string()])
);
}
#[test]
fn unknown_names_are_sorted_and_deduped() {
assert_eq!(
resolve_scope(Some("z,ghost,ghost"), &known()),
Err(vec!["ghost".to_string(), "z".to_string()])
);
}
#[test]
fn partially_unknown_list_is_rejected_whole() {
assert_eq!(
resolve_scope(Some("repo-a,ghost"), &known()),
Err(vec!["ghost".to_string()])
);
}
#[test]
fn sentinel_skips_membership_check() {
assert_eq!(resolve_scope(Some("all"), &[]), Ok(RepoScope::All));
assert_eq!(resolve_scope(Some("*"), &[]), Ok(RepoScope::All));
}
#[test]
fn sentinel_wins_over_unknown_name_in_list() {
assert_eq!(
resolve_scope(Some("all,ghost"), &known()),
Ok(RepoScope::All)
);
}
#[test]
fn repo_names_are_case_sensitive() {
assert_eq!(
resolve_scope(Some("REPO-A"), &known()),
Err(vec!["REPO-A".to_string()])
);
}
#[test]
fn clamp_defaults_to_five() {
assert_eq!(clamp_max_results(None), 5);
assert_eq!(clamp_max_results(Some(5)), 5);
}
#[test]
fn clamp_floor_is_one() {
assert_eq!(clamp_max_results(Some(0)), 1);
}
#[test]
fn clamp_ceiling_is_hundred() {
assert_eq!(clamp_max_results(Some(99999)), 100);
assert_eq!(clamp_max_results(Some(100)), 100);
}
}