use std::collections::BTreeMap;
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use crate::capability::{CapabilitySet, Determinism, Scope, SideEffects};
use crate::errors::PluginError;
use crate::plugin::PluginId;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct AbiRange(String);
impl AbiRange {
pub fn parse(s: impl AsRef<str>) -> Result<Self, PluginError> {
let s = s.as_ref();
VersionReq::parse(s)
.map_err(|e| PluginError::ManifestParse(format!("invalid abi range `{s}`: {e}")))?;
Ok(Self(s.to_owned()))
}
#[must_use]
pub fn matches(&self, host_major: u64) -> bool {
let req = VersionReq::parse(&self.0).unwrap_or(VersionReq::STAR);
let probe = Version::new(host_major, u64::MAX / 2, u64::MAX / 2);
req.matches(&probe)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PluginDep {
pub id: PluginId,
pub version_req: String,
#[serde(default)]
pub optional: bool,
}
impl PluginDep {
#[must_use]
pub fn new(id: PluginId, version_req: impl Into<String>) -> Self {
Self {
id,
version_req: version_req.into(),
optional: false,
}
}
#[must_use]
pub fn satisfied_by(&self, version: &Version) -> bool {
VersionReq::parse(&self.version_req)
.map(|r| r.matches(version))
.unwrap_or(false)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct ProvidedSurfaces {
pub scalar_fns: Vec<SmolStr>,
pub aggregate_fns: Vec<SmolStr>,
pub window_fns: Vec<SmolStr>,
pub procedures: Vec<SmolStr>,
pub locy_aggregates: Vec<SmolStr>,
pub locy_predicates: Vec<SmolStr>,
pub algorithms: Vec<SmolStr>,
pub storage_backends: Vec<SmolStr>,
pub index_kinds: Vec<SmolStr>,
pub crdt_kinds: Vec<SmolStr>,
pub logical_types: Vec<SmolStr>,
pub hooks: bool,
pub triggers: bool,
pub background_jobs: bool,
pub connectors: Vec<SmolStr>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PluginManifest {
pub id: PluginId,
pub version: Version,
pub abi: AbiRange,
#[serde(default)]
pub depends_on: Vec<PluginDep>,
#[serde(default)]
pub capabilities: CapabilitySet,
#[serde(default)]
pub determinism: Determinism,
#[serde(default)]
pub side_effects: SideEffects,
#[serde(default)]
pub scope: Scope,
#[serde(default)]
pub hash: Option<String>,
#[serde(default)]
pub signature: Option<ManifestSignature>,
#[serde(default)]
pub provides: ProvidedSurfaces,
#[serde(default)]
pub docs: String,
#[serde(default)]
pub metadata: BTreeMap<String, String>,
}
impl PluginManifest {
pub fn from_toml(s: impl AsRef<str>) -> Result<Self, PluginError> {
toml::from_str(s.as_ref()).map_err(|e| PluginError::ManifestParse(format!("toml: {e}")))
}
pub fn from_json(s: impl AsRef<str>) -> Result<Self, PluginError> {
serde_json::from_str(s.as_ref())
.map_err(|e| PluginError::ManifestParse(format!("json: {e}")))
}
pub fn to_toml(&self) -> Result<String, PluginError> {
toml::to_string_pretty(self)
.map_err(|e| PluginError::ManifestParse(format!("toml serialize: {e}")))
}
pub fn to_json(&self) -> Result<String, PluginError> {
serde_json::to_string(self)
.map_err(|e| PluginError::ManifestParse(format!("json serialize: {e}")))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ManifestSignature {
pub algorithm: String,
pub key_id: String,
pub value: String,
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_manifest() -> PluginManifest {
PluginManifest {
id: PluginId::new("ai.dragonscale.geo"),
version: Version::parse("0.3.1").unwrap(),
abi: AbiRange::parse("^1").unwrap(),
depends_on: vec![],
capabilities: CapabilitySet::new(),
determinism: Determinism::Pure,
side_effects: SideEffects::ReadOnly,
scope: Scope::Instance,
hash: None,
signature: None,
provides: ProvidedSurfaces::default(),
docs: String::new(),
metadata: BTreeMap::new(),
}
}
#[test]
fn abi_range_parse_and_match() {
let r = AbiRange::parse("^1.2").unwrap();
assert!(r.matches(1));
assert!(!r.matches(2));
}
#[test]
fn abi_range_rejects_garbage() {
assert!(AbiRange::parse("not-semver").is_err());
}
#[test]
fn manifest_round_trip_json() {
let m = sample_manifest();
let s = m.to_json().unwrap();
let parsed = PluginManifest::from_json(&s).unwrap();
assert_eq!(parsed, m);
}
#[test]
fn manifest_round_trip_toml() {
let m = sample_manifest();
let s = m.to_toml().unwrap();
let parsed = PluginManifest::from_toml(&s).unwrap();
assert_eq!(parsed, m);
}
#[test]
fn plugin_dep_version_satisfaction() {
let dep = PluginDep::new(PluginId::new("units"), "^0.4".to_owned());
assert!(dep.satisfied_by(&Version::parse("0.4.2").unwrap()));
assert!(!dep.satisfied_by(&Version::parse("0.3.0").unwrap()));
}
}