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}