use mlua::{FromLua, Lua, Table, Value};
use std::collections::{BTreeMap, BTreeSet};
use crate::error::Result;
use crate::error::VfoxError;
#[derive(Debug, Clone)]
pub struct Metadata {
pub name: String,
pub legacy_filenames: Vec<String>,
pub depends: Vec<String>,
pub version: String,
pub description: Option<String>,
pub author: Option<String>,
pub license: Option<String>,
pub homepage: Option<String>,
pub system_dependencies: Vec<SystemDependency>,
pub hooks: BTreeSet<&'static str>,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct SystemDependency {
pub bin: Option<String>,
pub pkgconfig: Option<String>,
pub sharedlib: Option<String>,
pub command: Option<String>,
pub version: Option<String>,
pub optional: Option<String>,
pub packages: BTreeMap<String, Vec<String>>,
}
struct PackageCandidates(Vec<String>);
impl FromLua for PackageCandidates {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
match value {
Value::Table(_) => Ok(PackageCandidates(Vec::<String>::from_lua(value, lua)?)),
other => Ok(PackageCandidates(vec![String::from_lua(other, lua)?])),
}
}
}
impl FromLua for SystemDependency {
fn from_lua(value: Value, _lua: &Lua) -> mlua::Result<Self> {
let table = value
.as_table()
.ok_or_else(|| mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: "SystemDependency".to_string(),
message: Some("each systemDependencies entry must be a table".to_string()),
})?;
Ok(SystemDependency {
bin: table.get("bin")?,
pkgconfig: table.get("pkgconfig")?,
sharedlib: table.get("sharedlib")?,
command: table.get("command")?,
version: table.get("version")?,
optional: table.get("optional")?,
packages: table
.get::<Option<BTreeMap<String, PackageCandidates>>>("packages")?
.unwrap_or_default()
.into_iter()
.map(|(mgr, candidates)| (mgr, candidates.0))
.collect(),
})
}
}
impl TryFrom<Table> for Metadata {
type Error = VfoxError;
fn try_from(t: Table) -> Result<Self> {
let legacy_filenames = t
.get::<Option<Vec<String>>>("legacyFilenames")?
.unwrap_or_default();
let depends = t.get::<Option<Vec<String>>>("depends")?.unwrap_or_default();
let system_dependencies = match t.get::<Option<Vec<SystemDependency>>>("systemDependencies")
{
Ok(deps) => deps.unwrap_or_default(),
Err(e) => {
warn!("ignoring malformed systemDependencies in plugin metadata: {e}");
vec![]
}
};
Ok(Metadata {
name: t.get("name")?,
legacy_filenames,
depends,
version: t.get("version")?,
description: t.get("description")?,
author: t.get("author")?,
license: t.get("license")?,
homepage: t.get("homepage")?,
system_dependencies,
hooks: Default::default(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn metadata_from_lua(code: &str) -> Metadata {
let lua = Lua::new();
lua.load(code).exec().unwrap();
let table: Table = lua.globals().get("PLUGIN").unwrap();
Metadata::try_from(table).unwrap()
}
fn metadata_result(code: &str) -> Result<Metadata> {
let lua = Lua::new();
lua.load(code).exec().unwrap();
let table: Table = lua.globals().get("PLUGIN").unwrap();
Metadata::try_from(table)
}
#[test]
fn test_depends_parsed() {
let m = metadata_from_lua(
r#"
PLUGIN = {
name = "test",
version = "1.0.0",
depends = {"node", "python"},
}
"#,
);
assert_eq!(m.depends, vec!["node", "python"]);
}
#[test]
fn test_depends_defaults_to_empty() {
let m = metadata_from_lua(
r#"
PLUGIN = {
name = "test",
version = "1.0.0",
}
"#,
);
assert!(m.depends.is_empty());
}
#[test]
fn test_system_dependencies_parsed() {
let m = metadata_from_lua(
r#"
PLUGIN = {
name = "test",
version = "1.0.0",
systemDependencies = {
{ bin = "bison", version = ">=3.0",
packages = { brew = "bison", apt = "bison" } },
{ pkgconfig = "libxml-2.0",
packages = { brew = "libxml2", apt = "libxml2-dev" } },
{ sharedlib = "libaio.so.1" },
{ command = "test -d /opt/thing", optional = "extra feature" },
},
}
"#,
);
assert_eq!(m.system_dependencies.len(), 4);
let bison = &m.system_dependencies[0];
assert_eq!(bison.bin.as_deref(), Some("bison"));
assert_eq!(bison.version.as_deref(), Some(">=3.0"));
assert_eq!(bison.packages.get("brew"), Some(&vec!["bison".to_string()]));
assert_eq!(bison.packages.get("apt"), Some(&vec!["bison".to_string()]));
assert_eq!(
m.system_dependencies[1].pkgconfig.as_deref(),
Some("libxml-2.0")
);
assert_eq!(
m.system_dependencies[2].sharedlib.as_deref(),
Some("libaio.so.1")
);
assert_eq!(
m.system_dependencies[3].optional.as_deref(),
Some("extra feature")
);
}
#[test]
fn test_system_dependencies_package_candidate_lists() {
let m = metadata_from_lua(
r#"
PLUGIN = {
name = "test",
version = "1.0.0",
systemDependencies = {
{ sharedlib = "libaio.so.1",
packages = { apt = { "libaio1t64", "libaio1" }, dnf = "libaio" } },
},
}
"#,
);
let dep = &m.system_dependencies[0];
assert_eq!(
dep.packages.get("apt"),
Some(&vec!["libaio1t64".to_string(), "libaio1".to_string()])
);
assert_eq!(dep.packages.get("dnf"), Some(&vec!["libaio".to_string()]));
}
#[test]
fn test_system_dependencies_defaults_to_empty() {
let m = metadata_from_lua(
r#"
PLUGIN = { name = "test", version = "1.0.0" }
"#,
);
assert!(m.system_dependencies.is_empty());
}
#[test]
fn test_system_dependencies_parse_is_lenient() {
let m = metadata_result(
r#"
PLUGIN = {
name = "test", version = "1.0.0",
depends = { "node" },
systemDependencies = {
{ version = ">=3.0" }, -- zero check keys
{ bin = "bison", pkgconfig = "libxml-2.0" }, -- two check keys
},
}
"#,
)
.unwrap();
assert_eq!(m.depends, vec!["node"]);
assert_eq!(m.system_dependencies.len(), 2);
}
#[test]
fn test_malformed_system_dependencies_does_not_break_metadata() {
let m = metadata_from_lua(
r#"
PLUGIN = {
name = "test", version = "1.0.0",
depends = { "node" },
systemDependencies = "not a table",
}
"#,
);
assert_eq!(m.depends, vec!["node"]);
assert!(m.system_dependencies.is_empty());
}
}