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}