use serde::{Deserialize, Serialize};
use std::path::Path;
use super::{PkgError, PkgResult};
pub const MANIFEST_FILE: &str = "znative.toml";
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PluginManifest {
#[serde(default)]
pub plugin: PluginMeta,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub native: Option<NativeSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub script: Option<ScriptSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PluginMeta {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub name: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub version: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct NativeSpec {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub lib: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ScriptSpec {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub source: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fpath: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum PluginKind {
Native(NativeSpec),
Script(ScriptSpec),
}
impl PluginManifest {
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> PkgResult<PluginManifest> {
toml::from_str::<PluginManifest>(s)
.map_err(|e| PkgError::Manifest(format!("znative.toml: {}", e.message())))
}
pub fn load(dir: &Path) -> PkgResult<Option<PluginManifest>> {
let path = dir.join(MANIFEST_FILE);
if !path.is_file() {
return Ok(None);
}
let s = std::fs::read_to_string(&path)
.map_err(|e| PkgError::Io(format!("read {}: {}", path.display(), e)))?;
Ok(Some(PluginManifest::from_str(&s)?))
}
}
impl PluginKind {
pub fn detect(dir: &Path, manifest: Option<&PluginManifest>) -> PkgResult<PluginKind> {
if let Some(m) = manifest {
if let Some(n) = &m.native {
return Ok(PluginKind::Native(n.clone()));
}
if let Some(s) = &m.script {
return Ok(PluginKind::Script(s.clone()));
}
}
if has_cdylib(dir) || cargo_is_cdylib(dir) {
return Ok(PluginKind::Native(NativeSpec::default()));
}
let mut source: Vec<String> = Vec::new();
let mut fpath: Vec<String> = Vec::new();
for entry in std::fs::read_dir(dir)? {
let name = entry?.file_name().to_string_lossy().into_owned();
if name.ends_with(".plugin.zsh") {
source.push(name);
}
}
if dir.join("functions").is_dir() {
fpath.push("functions".into());
}
if source.is_empty() {
if let Some(base) = dir.file_name().and_then(|s| s.to_str()) {
let cand = format!("{}.zsh", base);
if dir.join(&cand).is_file() {
source.push(cand);
}
}
}
if source.is_empty() && fpath.is_empty() {
for entry in std::fs::read_dir(dir)? {
let name = entry?.file_name().to_string_lossy().into_owned();
if name.ends_with(".zsh") {
source.push(name);
}
}
}
if source.is_empty() && fpath.is_empty() {
return Err(PkgError::Unknown(
"could not determine plugin kind: no znative.toml, no *.plugin.zsh, \
no functions/, and no Rust cdylib/Cargo.toml"
.into(),
));
}
source.sort();
Ok(PluginKind::Script(ScriptSpec { source, fpath }))
}
}
fn has_cdylib(dir: &Path) -> bool {
let Ok(rd) = std::fs::read_dir(dir) else {
return false;
};
for entry in rd.flatten() {
let n = entry.file_name();
let n = n.to_string_lossy();
if n.starts_with("lib") && (n.ends_with(".dylib") || n.ends_with(".so")) {
return true;
}
}
false
}
fn cargo_is_cdylib(dir: &Path) -> bool {
let cargo = dir.join("Cargo.toml");
let Ok(s) = std::fs::read_to_string(&cargo) else {
return false;
};
s.contains("cdylib")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_native_manifest() {
let m = PluginManifest::from_str(
"[plugin]\nname='x'\nversion='0.1.0'\n[native]\nlib='foo'\n",
)
.unwrap();
assert_eq!(m.plugin.name, "x");
assert_eq!(m.native.unwrap().lib, "foo");
}
#[test]
fn parses_script_manifest() {
let m = PluginManifest::from_str(
"[plugin]\nname='y'\n[script]\nsource=['y.plugin.zsh']\nfpath=['fns']\n",
)
.unwrap();
let s = m.script.unwrap();
assert_eq!(s.source, vec!["y.plugin.zsh"]);
assert_eq!(s.fpath, vec!["fns"]);
}
}