plux_rs/
bundle.rs

1use std::{cmp::Ordering, ffi::OsStr, fmt::Display};
2
3use semver::Version;
4use serde::{Deserialize, Serialize};
5
6use crate::{Depend, Info, Plugin, utils::BundleFromError};
7
8/// Represents a plugin bundle with its metadata.
9///
10/// A Bundle contains the essential information needed to identify and manage a plugin,
11/// including its unique identifier, version, and format. This information is used throughout
12/// the Plux system for plugin discovery, dependency resolution, and lifecycle management.
13///
14/// # Fields
15///
16/// * `id` - Unique identifier for the plugin (e.g., "calculator", "logger")
17/// * `version` - Semantic version of the plugin (e.g., "1.0.0")
18/// * `format` - File format/extension of the plugin (e.g., "lua", "rs", "wasm")
19///
20/// # Format
21///
22/// Plugin bundles follow the naming convention: `{id}-v{version}.{format}`
23/// For example: `calculator-v1.0.0.lua` or `renderer-v2.1.0.wasm`
24#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)]
25pub struct Bundle {
26    /// Unique identifier for the plugin
27    pub id: String,
28    /// Semantic version of the plugin
29    pub version: Version,
30    /// File format/extension of the plugin
31    pub format: String,
32}
33
34impl Bundle {
35    /// Creates a Bundle from a filename string.
36    ///
37    /// Parses a plugin filename following the standard Plux naming convention
38    /// `{id}-v{version}.{format}` and extracts the bundle information.
39    ///
40    /// # Parameters
41    ///
42    /// * `filename` - The filename to parse (e.g., "calculator-v1.0.0.lua")
43    ///
44    /// # Returns
45    ///
46    /// Returns `Result<Self, BundleFromError>` containing the parsed Bundle on success,
47    /// or an error if the filename doesn't match the expected format.
48    ///
49    /// # Examples
50    ///
51    /// ```rust
52    /// use plux_rs::Bundle;
53    ///
54    /// let bundle = Bundle::from_filename("my_plugin-v1.2.3.lua")?;
55    /// assert_eq!(bundle.id, "my_plugin");
56    /// assert_eq!(bundle.version.to_string(), "1.2.3");
57    /// assert_eq!(bundle.format, "lua");
58    /// # Ok::<(), Box<dyn std::error::Error>>(())
59    /// ```
60    ///
61    /// # Errors
62    ///
63    /// This function will return an error if:
64    /// - The filename cannot be converted to a string
65    /// - The filename doesn't contain a format extension
66    /// - The filename doesn't contain a version marker "-v"
67    /// - The ID, version, or format parts are empty
68    /// - The version string is not a valid semantic version
69    pub fn from_filename<S>(filename: &S) -> Result<Self, BundleFromError>
70    where
71        S: AsRef<OsStr> + ?Sized,
72    {
73        let mut path = filename
74            .as_ref()
75            .to_str()
76            .ok_or(BundleFromError::OsStrToStrFailed)?
77            .to_string();
78
79        let format = path
80            .drain(path.rfind('.').ok_or(BundleFromError::FormatFailed)? + 1..)
81            .collect::<String>();
82        let version = path
83            .drain(path.rfind("-v").ok_or(BundleFromError::VersionFailed)? + 2..path.len() - 1)
84            .collect::<String>();
85        let id = path
86            .drain(..path.rfind("-v").ok_or(BundleFromError::IDFailed)?)
87            .collect::<String>();
88
89        if format.is_empty() {
90            return Err(BundleFromError::FormatFailed);
91        }
92        if version.is_empty() {
93            return Err(BundleFromError::VersionFailed);
94        }
95        if id.is_empty() {
96            return Err(BundleFromError::IDFailed);
97        }
98
99        Ok(Self {
100            id,
101            version: Version::parse(version.as_str())?,
102            format,
103        })
104    }
105}
106
107impl<ID: AsRef<str>> PartialEq<(ID, &Version)> for Bundle {
108    fn eq(&self, (id, version): &(ID, &Version)) -> bool {
109        self.id == *id.as_ref() && self.version == **version
110    }
111}
112
113impl<O: Send + Sync, I: Info> PartialEq<Plugin<'_, O, I>> for Bundle {
114    fn eq(&self, other: &Plugin<'_, O, I>) -> bool {
115        self.id == other.info.bundle.id && self.version == other.info.bundle.version
116    }
117}
118
119impl PartialEq<Depend> for Bundle {
120    fn eq(&self, Depend { id: name, version }: &Depend) -> bool {
121        self.id == *name && version.matches(&self.version)
122    }
123}
124
125impl<ID: AsRef<str>> PartialOrd<(ID, &Version)> for Bundle {
126    fn partial_cmp(&self, (id, version): &(ID, &Version)) -> Option<Ordering> {
127        match self.id == *id.as_ref() {
128            true => self.version.partial_cmp(*version),
129            false => None,
130        }
131    }
132}
133
134impl<O: Send + Sync, I: Info> PartialOrd<Plugin<'_, O, I>> for Bundle {
135    fn partial_cmp(&self, other: &Plugin<'_, O, I>) -> Option<Ordering> {
136        match self.id == other.info.bundle.id {
137            true => self.version.partial_cmp(&other.info.bundle.version),
138            false => None,
139        }
140    }
141}
142
143impl Display for Bundle {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        write!(f, "{}-v{}.{}", self.id, self.version, self.format)
146    }
147}