codex_plugin/
plugin_id.rs1#[derive(Debug, thiserror::Error)]
4pub enum PluginIdError {
5 #[error("{0}")]
6 Invalid(String),
7}
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct PluginId {
11 pub plugin_name: String,
12 pub marketplace_name: String,
13}
14
15impl PluginId {
16 pub fn new(plugin_name: String, marketplace_name: String) -> Result<Self, PluginIdError> {
17 validate_plugin_segment(&plugin_name, "plugin name").map_err(PluginIdError::Invalid)?;
18 validate_plugin_segment(&marketplace_name, "marketplace name")
19 .map_err(PluginIdError::Invalid)?;
20 Ok(Self {
21 plugin_name,
22 marketplace_name,
23 })
24 }
25
26 pub fn parse(plugin_key: &str) -> Result<Self, PluginIdError> {
27 let Some((plugin_name, marketplace_name)) = plugin_key.rsplit_once('@') else {
28 return Err(PluginIdError::Invalid(format!(
29 "invalid plugin key `{plugin_key}`; expected <plugin>@<marketplace>"
30 )));
31 };
32 if plugin_name.is_empty() || marketplace_name.is_empty() {
33 return Err(PluginIdError::Invalid(format!(
34 "invalid plugin key `{plugin_key}`; expected <plugin>@<marketplace>"
35 )));
36 }
37
38 Self::new(plugin_name.to_string(), marketplace_name.to_string()).map_err(|err| match err {
39 PluginIdError::Invalid(message) => {
40 PluginIdError::Invalid(format!("{message} in `{plugin_key}`"))
41 }
42 })
43 }
44
45 pub fn as_key(&self) -> String {
46 format!("{}@{}", self.plugin_name, self.marketplace_name)
47 }
48}
49
50pub fn validate_plugin_segment(segment: &str, kind: &str) -> Result<(), String> {
52 if segment.is_empty() {
53 return Err(format!("invalid {kind}: must not be empty"));
54 }
55 if !segment
56 .chars()
57 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_')
58 {
59 return Err(format!(
60 "invalid {kind}: only ASCII letters, digits, `_`, and `-` are allowed"
61 ));
62 }
63 Ok(())
64}