use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::error::{Result, ToriiError};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlatformEntry {
pub name: String,
pub kind: String,
pub domain: String,
pub api_base_url: String,
pub web_base_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
}
#[derive(Debug, Default, Deserialize, Serialize)]
struct OnDisk {
#[serde(default, rename = "platform")]
platforms: Vec<PlatformEntry>,
}
fn global_path() -> Option<PathBuf> {
dirs::config_dir().map(|d| d.join("torii").join("platforms.toml"))
}
fn local_path<P: AsRef<Path>>(repo_path: P) -> PathBuf {
repo_path.as_ref().join(".torii").join("platforms.toml")
}
fn load_file(path: &Path) -> Vec<PlatformEntry> {
if !path.exists() {
return Vec::new();
}
let Ok(text) = fs::read_to_string(path) else { return Vec::new() };
let Ok(parsed) = toml::from_str::<OnDisk>(&text) else { return Vec::new() };
parsed.platforms
}
fn save_file(path: &Path, entries: &[PlatformEntry]) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| ToriiError::Fs(format!("mkdir {}: {}", parent.display(), e)))?;
}
let on_disk = OnDisk { platforms: entries.to_vec() };
let text = toml::to_string_pretty(&on_disk)
.map_err(|e| ToriiError::InvalidConfig(format!("serialise platforms.toml: {}", e)))?;
fs::write(path, text)
.map_err(|e| ToriiError::Fs(format!("write {}: {}", path.display(), e)))?;
Ok(())
}
pub fn load_global() -> Vec<PlatformEntry> {
global_path().map(|p| load_file(&p)).unwrap_or_default()
}
pub fn load_local<P: AsRef<Path>>(repo_path: P) -> Vec<PlatformEntry> {
load_file(&local_path(repo_path))
}
pub fn merged<P: AsRef<Path>>(repo_path: P) -> Vec<PlatformEntry> {
let mut by_name: BTreeMap<String, PlatformEntry> = BTreeMap::new();
for e in load_global() { by_name.insert(e.name.clone(), e); }
for e in load_local(repo_path){ by_name.insert(e.name.clone(), e); }
by_name.into_values().collect()
}
pub fn builtins() -> Vec<PlatformEntry> {
vec![
PlatformEntry {
name: "github.com".into(),
kind: "github".into(),
domain: "github.com".into(),
api_base_url: "https://api.github.com".into(),
web_base_url: "https://github.com".into(),
client_id: None,
},
PlatformEntry {
name: "gitlab.com".into(),
kind: "gitlab".into(),
domain: "gitlab.com".into(),
api_base_url: "https://gitlab.com/api/v4".into(),
web_base_url: "https://gitlab.com".into(),
client_id: None,
},
PlatformEntry {
name: "codeberg.org".into(),
kind: "codeberg".into(),
domain: "codeberg.org".into(),
api_base_url: "https://codeberg.org/api/v1".into(),
web_base_url: "https://codeberg.org".into(),
client_id: None,
},
PlatformEntry {
name: "bitbucket.org".into(),
kind: "bitbucket".into(),
domain: "bitbucket.org".into(),
api_base_url: "https://api.bitbucket.org/2.0".into(),
web_base_url: "https://bitbucket.org".into(),
client_id: None,
},
]
}
pub fn all<P: AsRef<Path>>(repo_path: P) -> Vec<PlatformEntry> {
let user = merged(repo_path);
let user_names: std::collections::BTreeSet<&str> =
user.iter().map(|e| e.name.as_str()).collect();
let mut out: Vec<PlatformEntry> = user.clone();
for b in builtins() {
if !user_names.contains(b.name.as_str()) {
out.push(b);
}
}
out
}
pub fn find_by_host<P: AsRef<Path>>(repo_path: P, host: &str) -> Option<PlatformEntry> {
let mut candidates = all(repo_path);
candidates.sort_by_key(|e| std::cmp::Reverse(e.domain.len()));
candidates.into_iter().find(|e| host == e.domain || host.ends_with(&format!(".{}", e.domain)))
}
pub fn add_entry<P: AsRef<Path>>(repo_path: P, entry: PlatformEntry, local: bool) -> Result<()> {
let path = if local { local_path(&repo_path) } else { global_path()
.ok_or_else(|| ToriiError::InvalidConfig("no config dir".into()))? };
let mut entries = load_file(&path);
entries.retain(|e| e.name != entry.name);
entries.push(entry);
save_file(&path, &entries)
}
pub fn remove_entry<P: AsRef<Path>>(repo_path: P, name: &str, local: bool) -> Result<bool> {
let path = if local { local_path(&repo_path) } else { global_path()
.ok_or_else(|| ToriiError::InvalidConfig("no config dir".into()))? };
let mut entries = load_file(&path);
let before = entries.len();
entries.retain(|e| e.name != name);
if entries.len() == before {
return Ok(false);
}
save_file(&path, &entries)?;
Ok(true)
}