Skip to main content

codex_plugin/
plugin_id.rs

1//! Stable plugin identifier parsing and validation shared with the plugin cache.
2
3#[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
50/// Validates a single path segment used in plugin IDs and cache layout.
51pub 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}