torii_lib/platforms/
registry.rs1use std::collections::BTreeMap;
21use std::fs;
22use std::path::{Path, PathBuf};
23
24use serde::{Deserialize, Serialize};
25
26use crate::error::{Result, ToriiError};
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct PlatformEntry {
32 pub name: String,
34 pub kind: String,
40 pub domain: String,
43 pub api_base_url: String,
46 pub web_base_url: String,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub client_id: Option<String>,
53}
54
55#[derive(Debug, Default, Deserialize, Serialize)]
57struct OnDisk {
58 #[serde(default, rename = "platform")]
59 platforms: Vec<PlatformEntry>,
60}
61
62fn global_path() -> Option<PathBuf> {
63 dirs::config_dir().map(|d| d.join("torii").join("platforms.toml"))
64}
65
66fn local_path<P: AsRef<Path>>(repo_path: P) -> PathBuf {
67 repo_path.as_ref().join(".torii").join("platforms.toml")
68}
69
70fn load_file(path: &Path) -> Vec<PlatformEntry> {
71 if !path.exists() {
72 return Vec::new();
73 }
74 let Ok(text) = fs::read_to_string(path) else { return Vec::new() };
75 let Ok(parsed) = toml::from_str::<OnDisk>(&text) else { return Vec::new() };
76 parsed.platforms
77}
78
79fn save_file(path: &Path, entries: &[PlatformEntry]) -> Result<()> {
80 if let Some(parent) = path.parent() {
81 fs::create_dir_all(parent)
82 .map_err(|e| ToriiError::Fs(format!("mkdir {}: {}", parent.display(), e)))?;
83 }
84 let on_disk = OnDisk { platforms: entries.to_vec() };
85 let text = toml::to_string_pretty(&on_disk)
86 .map_err(|e| ToriiError::InvalidConfig(format!("serialise platforms.toml: {}", e)))?;
87 fs::write(path, text)
88 .map_err(|e| ToriiError::Fs(format!("write {}: {}", path.display(), e)))?;
89 Ok(())
90}
91
92pub fn load_global() -> Vec<PlatformEntry> {
94 global_path().map(|p| load_file(&p)).unwrap_or_default()
95}
96
97pub fn load_local<P: AsRef<Path>>(repo_path: P) -> Vec<PlatformEntry> {
99 load_file(&local_path(repo_path))
100}
101
102pub fn merged<P: AsRef<Path>>(repo_path: P) -> Vec<PlatformEntry> {
106 let mut by_name: BTreeMap<String, PlatformEntry> = BTreeMap::new();
107 for e in load_global() { by_name.insert(e.name.clone(), e); }
108 for e in load_local(repo_path){ by_name.insert(e.name.clone(), e); }
109 by_name.into_values().collect()
110}
111
112pub fn builtins() -> Vec<PlatformEntry> {
117 vec![
118 PlatformEntry {
119 name: "github.com".into(),
120 kind: "github".into(),
121 domain: "github.com".into(),
122 api_base_url: "https://api.github.com".into(),
123 web_base_url: "https://github.com".into(),
124 client_id: None,
125 },
126 PlatformEntry {
127 name: "gitlab.com".into(),
128 kind: "gitlab".into(),
129 domain: "gitlab.com".into(),
130 api_base_url: "https://gitlab.com/api/v4".into(),
131 web_base_url: "https://gitlab.com".into(),
132 client_id: None,
133 },
134 PlatformEntry {
135 name: "codeberg.org".into(),
136 kind: "codeberg".into(),
137 domain: "codeberg.org".into(),
138 api_base_url: "https://codeberg.org/api/v1".into(),
139 web_base_url: "https://codeberg.org".into(),
140 client_id: None,
141 },
142 PlatformEntry {
143 name: "bitbucket.org".into(),
144 kind: "bitbucket".into(),
145 domain: "bitbucket.org".into(),
146 api_base_url: "https://api.bitbucket.org/2.0".into(),
147 web_base_url: "https://bitbucket.org".into(),
148 client_id: None,
149 },
150 ]
151}
152
153pub fn all<P: AsRef<Path>>(repo_path: P) -> Vec<PlatformEntry> {
157 let user = merged(repo_path);
158 let user_names: std::collections::BTreeSet<&str> =
159 user.iter().map(|e| e.name.as_str()).collect();
160 let mut out: Vec<PlatformEntry> = user.clone();
161 for b in builtins() {
162 if !user_names.contains(b.name.as_str()) {
163 out.push(b);
164 }
165 }
166 out
167}
168
169pub fn find_by_host<P: AsRef<Path>>(repo_path: P, host: &str) -> Option<PlatformEntry> {
173 let mut candidates = all(repo_path);
174 candidates.sort_by_key(|e| std::cmp::Reverse(e.domain.len()));
177 candidates.into_iter().find(|e| host == e.domain || host.ends_with(&format!(".{}", e.domain)))
178}
179
180pub fn add_entry<P: AsRef<Path>>(repo_path: P, entry: PlatformEntry, local: bool) -> Result<()> {
183 let path = if local { local_path(&repo_path) } else { global_path()
184 .ok_or_else(|| ToriiError::InvalidConfig("no config dir".into()))? };
185 let mut entries = load_file(&path);
186 entries.retain(|e| e.name != entry.name);
187 entries.push(entry);
188 save_file(&path, &entries)
189}
190
191pub fn remove_entry<P: AsRef<Path>>(repo_path: P, name: &str, local: bool) -> Result<bool> {
196 let path = if local { local_path(&repo_path) } else { global_path()
197 .ok_or_else(|| ToriiError::InvalidConfig("no config dir".into()))? };
198 let mut entries = load_file(&path);
199 let before = entries.len();
200 entries.retain(|e| e.name != name);
201 if entries.len() == before {
202 return Ok(false);
203 }
204 save_file(&path, &entries)?;
205 Ok(true)
206}