use crate::capability::Capability;
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
pub const API_VERSION: &str = "0.1.0";
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PluginId(pub String);
impl PluginId {
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_valid(&self) -> bool {
let s = &self.0;
!s.is_empty()
&& s.len() <= 128
&& s.split('.').count() >= 2
&& s.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '.' || c == '-')
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct SignalId(pub String);
impl SignalId {
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ApiVersion(pub VersionReq);
impl ApiVersion {
pub fn is_satisfied_by_host(&self) -> bool {
Version::parse(API_VERSION)
.map(|v| self.0.matches(&v))
.unwrap_or(false)
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct GameMatcher {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub steam_appid: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub executable: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub window_title: Option<String>,
}
impl GameMatcher {
pub fn is_empty(&self) -> bool {
self.steam_appid.is_none() && self.executable.is_none() && self.window_title.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SignalDecl {
pub id: SignalId,
pub value_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unit: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PluginManifest {
pub id: PluginId,
pub name: String,
pub version: Version,
pub api_version: ApiVersion,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authors: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
pub game: GameMatcher,
#[serde(default)]
pub multiplayer: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub signals: Vec<SignalDecl>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub intents: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capabilities: Vec<Capability>,
}
impl PluginManifest {
pub fn validate(&self) -> Result<(), crate::PluginError> {
use crate::PluginError::InvalidManifest;
if !self.id.is_valid() {
return Err(InvalidManifest(format!(
"id `{}` must be reverse-DNS, lowercase, e.g. `dev.bryan.mygame`",
self.id.0
)));
}
if self.name.trim().is_empty() {
return Err(InvalidManifest("name must not be empty".into()));
}
if self.game.is_empty() {
return Err(InvalidManifest(
"game matcher must set at least one of steam_appid, executable, window_title"
.into(),
));
}
let mut ids: Vec<&SignalId> = self.signals.iter().map(|s| &s.id).collect();
ids.sort();
let before = ids.len();
ids.dedup();
if ids.len() != before {
return Err(InvalidManifest("duplicate signal id in schema".into()));
}
Ok(())
}
}