Skip to main content

zeph_plugins/
types.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Validated newtypes for plugin domain values.
5
6use std::fmt;
7
8use crate::PluginError;
9use crate::manager::validate_plugin_name;
10
11/// A validated plugin name.
12///
13/// A `PluginName` is guaranteed to satisfy the plugin naming rules:
14/// `[a-z][a-z0-9-]*`, at most 64 characters, no path separators or dots.
15///
16/// Construct via [`TryFrom<String>`] or [`TryFrom<&str>`]; both delegate to the
17/// same `validate_plugin_name` predicate used throughout the plugin manager.
18///
19/// # Examples
20///
21/// ```rust
22/// use zeph_plugins::PluginName;
23///
24/// let name: PluginName = "my-plugin".try_into().unwrap();
25/// assert_eq!(name.as_str(), "my-plugin");
26/// assert_eq!(name.to_string(), "my-plugin");
27/// ```
28#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
29#[serde(transparent)]
30pub struct PluginName(String);
31
32impl<'de> serde::Deserialize<'de> for PluginName {
33    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
34        let s = String::deserialize(d)?;
35        PluginName::try_from(s).map_err(serde::de::Error::custom)
36    }
37}
38
39impl PluginName {
40    /// Returns the plugin name as a `&str`.
41    ///
42    /// # Examples
43    ///
44    /// ```rust
45    /// use zeph_plugins::PluginName;
46    ///
47    /// let name: PluginName = "cool-plugin".try_into().unwrap();
48    /// assert_eq!(name.as_str(), "cool-plugin");
49    /// ```
50    #[must_use]
51    pub fn as_str(&self) -> &str {
52        &self.0
53    }
54}
55
56impl TryFrom<String> for PluginName {
57    type Error = PluginError;
58
59    /// Validate and wrap a plugin name.
60    ///
61    /// # Errors
62    ///
63    /// Returns [`PluginError::InvalidName`] when the string does not satisfy the
64    /// naming rules: `[a-z][a-z0-9-]*`, at most 64 characters, no path separators
65    /// or dots.
66    ///
67    /// # Examples
68    ///
69    /// ```rust
70    /// use zeph_plugins::PluginName;
71    ///
72    /// let ok: PluginName = PluginName::try_from("valid-name".to_owned()).unwrap();
73    /// assert_eq!(ok.as_str(), "valid-name");
74    ///
75    /// // Uppercase letters are rejected.
76    /// let err = PluginName::try_from("Invalid_Name".to_owned());
77    /// assert!(err.is_err());
78    ///
79    /// // Empty string is rejected.
80    /// let err = PluginName::try_from(String::new());
81    /// assert!(err.is_err());
82    ///
83    /// // Names longer than 64 characters are rejected.
84    /// let err = PluginName::try_from("a".repeat(65));
85    /// assert!(err.is_err());
86    /// ```
87    fn try_from(value: String) -> Result<Self, Self::Error> {
88        validate_plugin_name(&value)?;
89        Ok(Self(value))
90    }
91}
92
93impl TryFrom<&str> for PluginName {
94    type Error = PluginError;
95
96    /// Validate and wrap a plugin name from a string slice.
97    ///
98    /// # Errors
99    ///
100    /// Returns [`PluginError::InvalidName`] when the string does not satisfy the
101    /// naming rules: `[a-z][a-z0-9-]*`, at most 64 characters, no path separators
102    /// or dots.
103    ///
104    /// # Examples
105    ///
106    /// ```rust
107    /// use zeph_plugins::PluginName;
108    ///
109    /// let ok: PluginName = PluginName::try_from("another-plugin").unwrap();
110    /// assert_eq!(ok.as_str(), "another-plugin");
111    ///
112    /// let err = PluginName::try_from("BAD");
113    /// assert!(err.is_err());
114    /// ```
115    fn try_from(value: &str) -> Result<Self, Self::Error> {
116        validate_plugin_name(value)?;
117        Ok(Self(value.to_owned()))
118    }
119}
120
121impl fmt::Display for PluginName {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        f.write_str(&self.0)
124    }
125}
126
127impl AsRef<str> for PluginName {
128    fn as_ref(&self) -> &str {
129        &self.0
130    }
131}
132
133impl PartialEq<str> for PluginName {
134    fn eq(&self, other: &str) -> bool {
135        self.0 == other
136    }
137}
138
139impl PartialEq<&str> for PluginName {
140    fn eq(&self, other: &&str) -> bool {
141        self.0 == *other
142    }
143}
144
145impl PartialEq<String> for PluginName {
146    fn eq(&self, other: &String) -> bool {
147        &self.0 == other
148    }
149}