use clap_complete::{
ArgValueCompleter, CompletionCandidate, PathCompleter,
engine::ValueCompleter,
};
use slumber_core::{
collection::{Collection, CollectionError, CollectionFile, ProfileId},
database::Database,
};
use std::{ffi::OsStr, ops::Deref};
use tracing::level_filters::LevelFilter;
pub fn complete_profile() -> ArgValueCompleter {
ArgValueCompleter::new(|current: &OsStr| {
load_collection()
.map(|collection| {
get_candidates(
collection.profiles.keys().map(ProfileId::to_string),
current,
)
})
.unwrap_or_default()
})
}
pub fn complete_recipe() -> ArgValueCompleter {
ArgValueCompleter::new(|current: &OsStr| {
load_collection()
.map(|collection| get_recipe_ids(&collection, current))
.unwrap_or_default()
})
}
pub fn complete_recipe_or_request_id() -> ArgValueCompleter {
ArgValueCompleter::new(move |current: &OsStr| {
let mut completions = Vec::new();
completions.extend(
load_collection()
.map(|collection| get_recipe_ids(&collection, current))
.unwrap_or_default(),
);
completions.extend(
Database::load()
.and_then(|db| db.get_all_requests())
.map(|exchanges| {
get_candidates(
exchanges
.into_iter()
.map(|exchange| exchange.id.to_string()),
current,
)
})
.unwrap_or_default(),
);
completions
})
}
pub fn complete_collection_path() -> ArgValueCompleter {
ArgValueCompleter::new(collection_path_completer())
}
pub fn complete_collection_specifier() -> ArgValueCompleter {
let path_completer = collection_path_completer();
ArgValueCompleter::new(move |current: &OsStr| {
let mut completions = Vec::new();
completions.extend(path_completer.complete(current));
completions.extend(
Database::load()
.and_then(|db| db.get_collections())
.map(|collections| {
get_candidates(
collections
.into_iter()
.map(|collection| collection.id.to_string()),
current,
)
})
.unwrap_or_default(),
);
completions
})
}
pub fn complete_log_level() -> ArgValueCompleter {
ArgValueCompleter::new(|current: &OsStr| {
get_candidates(
[
LevelFilter::OFF,
LevelFilter::ERROR,
LevelFilter::WARN,
LevelFilter::INFO,
LevelFilter::DEBUG,
LevelFilter::TRACE,
]
.into_iter()
.map(|l| l.to_string()),
current,
)
})
}
fn load_collection() -> Result<Collection, CollectionError> {
let collection_file = CollectionFile::new(None)?;
collection_file.load()
}
fn collection_path_completer() -> PathCompleter {
PathCompleter::file().filter(|path| {
let extension = path.extension();
extension == Some(OsStr::new("yml"))
|| extension == Some(OsStr::new("yaml"))
})
}
fn get_recipe_ids(
collection: &Collection,
current: &OsStr,
) -> Vec<CompletionCandidate> {
get_candidates(
collection
.recipes
.iter()
.filter_map(|(_, node)| Some(node.recipe()?.id.to_string())),
current,
)
}
fn get_candidates<T: Into<String>>(
iter: impl Iterator<Item = T>,
current: &OsStr,
) -> Vec<CompletionCandidate> {
let Some(current) = current.to_str() else {
return Vec::new();
};
iter.map(T::into)
.filter(|value| value.starts_with(current))
.map(|value| CompletionCandidate::new(value.deref()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use env_lock::CurrentDirGuard;
use rstest::{fixture, rstest};
use slumber_core::http::{Exchange, RequestId};
use slumber_util::{DataDir, Factory, data_dir};
use std::path::{Path, PathBuf};
#[rstest]
fn test_complete_profile(_current_dir: CurrentDirGuard) {
let completions = complete(complete_profile());
assert_eq!(&completions, &["profile1", "profile2"]);
}
#[rstest]
fn test_complete_recipe(_current_dir: CurrentDirGuard) {
let completions = complete(complete_recipe());
assert_eq!(&completions[..3], &["getUser", "query", "headers",]);
}
#[rstest]
fn test_complete_recipe_or_request_id(
_current_dir: CurrentDirGuard,
database: TestDatabase,
) {
let id1 = RequestId::new();
let id2 = RequestId::new();
let collection_db = database
.database
.into_collection(&collection_file())
.unwrap();
for id in [id1, id2] {
collection_db
.insert_exchange(&Exchange::factory(id))
.unwrap();
}
let mut completions = complete(complete_recipe_or_request_id());
completions.drain(3..completions.len() - 2);
assert_eq!(
&completions,
&[
"getUser",
"query",
"headers",
&id2.to_string(),
&id1.to_string()
]
);
}
#[rstest]
fn test_complete_collection_path(_current_dir: CurrentDirGuard) {
let completions = complete(complete_collection_path());
assert_eq!(&completions, &["other.yml", "slumber.yml"]);
}
#[rstest]
fn test_complete_collection_specifier(
_current_dir: CurrentDirGuard,
database: TestDatabase,
) {
let collection_db = database
.database
.into_collection(&collection_file())
.unwrap();
let completions = complete(complete_collection_specifier());
assert_eq!(
&completions,
&[
"other.yml",
"slumber.yml",
&collection_db.collection_id().to_string()
]
);
}
#[test]
fn test_get_candidates() {
let candidates: Vec<String> = get_candidates(
["abc123", "abc", "bca"].into_iter(),
OsStr::new("abc"),
)
.into_iter()
.map(|candidate| candidate.get_value().to_str().unwrap().to_owned())
.collect();
assert_eq!(candidates, &["abc123", "abc"]);
}
fn complete(completer: ArgValueCompleter) -> Vec<String> {
completer
.complete(OsStr::new(""))
.into_iter()
.map(|completion| {
completion.get_value().to_str().unwrap().to_owned()
})
.collect()
}
struct TestDatabase {
database: Database,
_data_dir: DataDir,
}
#[fixture]
fn database(data_dir: DataDir) -> TestDatabase {
let database = Database::load().unwrap();
TestDatabase {
database,
_data_dir: data_dir,
}
}
fn tests_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests")
}
fn collection_file() -> CollectionFile {
CollectionFile::with_dir(tests_dir(), None).unwrap()
}
#[fixture]
fn current_dir() -> CurrentDirGuard {
env_lock::lock_current_dir(tests_dir()).unwrap()
}
}