use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum InstallScope {
User,
Project,
Local,
}
impl Default for InstallScope {
fn default() -> Self {
Self::User
}
}
impl std::fmt::Display for InstallScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InstallScope::User => write!(f, "user"),
InstallScope::Project => write!(f, "project"),
InstallScope::Local => write!(f, "local"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketplacesFile {
pub version: u32,
#[serde(default)]
pub marketplaces: BTreeMap<String, MarketplaceEntry>,
}
impl Default for MarketplacesFile {
fn default() -> Self {
Self { version: 1, marketplaces: BTreeMap::new() }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketplaceEntry {
pub source: String,
pub added_at: String,
pub git_commit: String,
#[serde(default)]
pub plugins: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstalledPluginsFile {
pub version: u32,
#[serde(default)]
pub plugins: BTreeMap<String, InstalledPluginEntry>,
}
impl Default for InstalledPluginsFile {
fn default() -> Self {
Self { version: 1, plugins: BTreeMap::new() }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstalledPluginEntry {
pub marketplace: String,
pub plugin: String,
pub plugin_dir: String,
pub installed_at: String,
#[serde(default)]
pub scope: InstallScope,
}
pub fn load_marketplaces_file(path: &Path) -> Result<MarketplacesFile> {
if !path.exists() {
return Ok(MarketplacesFile::default());
}
let raw = std::fs::read_to_string(path)
.with_context(|| format!("read {}", path.display()))?;
let parsed: MarketplacesFile = serde_json::from_str(&raw)
.with_context(|| format!("parse {}", path.display()))?;
Ok(parsed)
}
pub fn save_marketplaces_file(path: &Path, file: &MarketplacesFile) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).ok();
}
let raw = serde_json::to_string_pretty(file)?;
std::fs::write(path, raw)?;
Ok(())
}
pub fn load_installed_plugins_file(path: &Path) -> Result<InstalledPluginsFile> {
if !path.exists() {
return Ok(InstalledPluginsFile::default());
}
let raw = std::fs::read_to_string(path)
.with_context(|| format!("read {}", path.display()))?;
let parsed: InstalledPluginsFile = serde_json::from_str(&raw)
.with_context(|| format!("parse {}", path.display()))?;
Ok(parsed)
}
pub fn save_installed_plugins_file(path: &Path, file: &InstalledPluginsFile) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).ok();
}
let raw = serde_json::to_string_pretty(file)?;
std::fs::write(path, raw)?;
Ok(())
}
pub fn plugin_id(plugin: &str, marketplace: &str) -> String {
format!("{}@{}", plugin, marketplace)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips_marketplaces_file() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("mp.json");
let mut f = MarketplacesFile::default();
f.marketplaces.insert(
"x".into(),
MarketplaceEntry {
source: "https://e/r.git".into(),
added_at: "now".into(),
git_commit: "abc".into(),
plugins: vec!["p".into()],
},
);
save_marketplaces_file(&path, &f).unwrap();
let loaded = load_marketplaces_file(&path).unwrap();
assert_eq!(loaded.marketplaces.len(), 1);
assert_eq!(loaded.marketplaces["x"].git_commit, "abc");
}
#[test]
fn missing_file_returns_default() {
let tmp = tempfile::tempdir().unwrap();
let f = load_marketplaces_file(&tmp.path().join("none")).unwrap();
assert!(f.marketplaces.is_empty());
let f = load_installed_plugins_file(&tmp.path().join("none")).unwrap();
assert!(f.plugins.is_empty());
}
#[test]
fn plugin_id_format() {
assert_eq!(plugin_id("a", "b"), "a@b");
}
}