use std::path::{Path, PathBuf};
use super::dirs::{cache_root, config_root, env_lookup, state_root};
use super::documents::documents_dir;
use super::migrate::{self, Migration};
const EXTENSION: &str = "conf";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Family {
id: &'static str,
title: &'static str,
}
impl Family {
pub const QUVYTA: Family = Family::new("quvyta", "Quvyta");
#[must_use]
pub const fn new(id: &'static str, title: &'static str) -> Self {
Self { id, title }
}
#[must_use]
pub fn id(&self) -> &'static str {
self.id
}
#[must_use]
pub fn title(&self) -> &'static str {
self.title
}
#[must_use]
pub fn config_dir(&self) -> Option<PathBuf> {
self.config_under(config_root(env_lookup))
}
#[must_use]
pub fn shared_file(&self) -> Option<PathBuf> {
self.config_dir().map(|dir| dir.join(file_name(self.id)))
}
#[must_use]
pub fn app_file(&self, app: &str) -> Option<PathBuf> {
self.config_dir().map(|dir| dir.join(file_name(app)))
}
#[must_use]
pub fn app_dir(&self, app: &str) -> Option<PathBuf> {
self.config_dir().map(|dir| dir.join(app))
}
#[must_use]
pub fn state_dir(&self, app: &str) -> Option<PathBuf> {
self.member_under(state_root(env_lookup), app)
}
#[must_use]
pub fn cache_dir(&self, app: &str) -> Option<PathBuf> {
self.member_under(cache_root(env_lookup), app)
}
#[must_use]
pub fn workspace_dir(&self, app_title: &str) -> Option<PathBuf> {
documents_dir().map(|documents| self.workspace_under(&documents, app_title))
}
#[must_use]
pub fn adopt(&self, app: &str, legacy_dir: &Path) -> Migration {
match self.config_dir() {
Some(config_dir) => self.adopt_in(&config_dir, app, legacy_dir),
None => Migration::without_folder(),
}
}
#[must_use]
pub fn adopt_in(&self, config_dir: &Path, app: &str, legacy_dir: &Path) -> Migration {
migrate::adopt(legacy_dir, &config_dir.join(file_name(app)), &config_dir.join(app))
}
fn member_under(&self, root: Option<PathBuf>, app: &str) -> Option<PathBuf> {
self.config_under(root).map(|family| family.join(app))
}
fn config_under(&self, root: Option<PathBuf>) -> Option<PathBuf> {
let name = if cfg!(any(windows, target_os = "macos")) { self.title } else { self.id };
root.map(|root| root.join(name))
}
fn workspace_under(&self, documents: &Path, app_title: &str) -> PathBuf {
documents.join(self.title).join(app_title)
}
}
pub(super) fn file_name(id: &str) -> String {
format!("{id}.{EXTENSION}")
}
#[cfg(test)]
mod tests {
use super::*;
fn env(pairs: &'static [(&'static str, &'static str)]) -> impl Fn(&str) -> Option<PathBuf> {
move |name: &str| pairs.iter().find(|(key, _)| *key == name).map(|(_, value)| PathBuf::from(value))
}
#[test]
fn the_family_folder_is_lowercase_on_unix() {
if !cfg!(all(unix, not(target_os = "macos"))) {
return;
}
let family = Family::QUVYTA;
let home = config_root(env(&[("HOME", "/home/ada")]));
assert_eq!(family.config_under(home), Some(PathBuf::from("/home/ada/.config/quvyta")));
let xdg = config_root(env(&[("HOME", "/home/ada"), ("XDG_CONFIG_HOME", "/cfg")]));
assert_eq!(family.config_under(xdg), Some(PathBuf::from("/cfg/quvyta")));
}
#[test]
fn the_family_folder_carries_the_title_on_macos_and_windows() {
if cfg!(target_os = "macos") {
let root = config_root(env(&[("HOME", "/Users/ada")]));
let expected = PathBuf::from("/Users/ada/Library/Application Support/Quvyta");
assert_eq!(Family::QUVYTA.config_under(root), Some(expected));
}
if cfg!(windows) {
let root = config_root(env(&[("APPDATA", r"C:\Users\ada\AppData\Roaming")]));
let expected = PathBuf::from(r"C:\Users\ada\AppData\Roaming\Quvyta");
assert_eq!(Family::QUVYTA.config_under(root), Some(expected));
}
}
#[test]
fn a_member_keeps_its_state_and_cache_under_the_family_folder() {
if !cfg!(all(unix, not(target_os = "macos"))) {
return;
}
let family = Family::QUVYTA;
let home = env(&[("HOME", "/home/ada")]);
assert_eq!(
family.member_under(state_root(&home), "packages"),
Some(PathBuf::from("/home/ada/.local/state/quvyta/packages"))
);
assert_eq!(
family.member_under(cache_root(&home), "packages"),
Some(PathBuf::from("/home/ada/.cache/quvyta/packages"))
);
let xdg = env(&[("HOME", "/home/ada"), ("XDG_STATE_HOME", "/st"), ("XDG_CACHE_HOME", "/ca")]);
assert_eq!(family.member_under(state_root(&xdg), "packages"), Some(PathBuf::from("/st/quvyta/packages")));
assert_eq!(family.member_under(cache_root(&xdg), "packages"), Some(PathBuf::from("/ca/quvyta/packages")));
let relative = env(&[("HOME", "/home/ada"), ("XDG_CACHE_HOME", "ca")]);
assert_eq!(
family.member_under(cache_root(&relative), "packages"),
Some(PathBuf::from("/home/ada/.cache/quvyta/packages"))
);
assert_eq!(family.member_under(cache_root(env(&[])), "packages"), None);
}
#[test]
fn the_public_state_and_cache_folders_end_in_family_and_member() {
let family = Family::QUVYTA;
for dir in [family.state_dir("packages"), family.cache_dir("packages")].into_iter().flatten() {
assert!(
dir.ends_with(Path::new(family.id()).join("packages"))
|| dir.ends_with(Path::new(family.title()).join("packages")),
"{}",
dir.display()
);
}
}
#[test]
fn no_home_means_no_family_folder() {
assert_eq!(Family::QUVYTA.config_under(config_root(env(&[]))), None);
}
#[test]
fn files_are_lowercase_ids_in_the_family_folder() {
let family = Family::new("tools", "Tools");
let Some(dir) = family.config_dir() else { return };
assert_eq!(family.shared_file(), Some(dir.join("tools.conf")));
assert_eq!(family.app_file("code"), Some(dir.join("code.conf")));
assert_eq!(family.app_dir("code"), Some(dir.join("code")));
assert_eq!((family.id(), family.title()), ("tools", "Tools"));
}
#[test]
fn the_workspace_is_the_family_and_app_titles_under_documents() {
let documents = Path::new("/home/ada/Belgeler");
let expected = PathBuf::from("/home/ada/Belgeler/Quvyta/Code");
assert_eq!(Family::QUVYTA.workspace_under(documents, "Code"), expected);
if let (Some(documents), Some(workspace)) = (documents_dir(), Family::QUVYTA.workspace_dir("Code")) {
assert_eq!(workspace, documents.join("Quvyta").join("Code"));
}
}
}