greentic_types/
component_source.rs

1//! Canonical component source references for packs.
2
3use alloc::string::{String, ToString};
4
5#[cfg(feature = "schemars")]
6use schemars::JsonSchema;
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9
10/// Supported component source references.
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
12#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13#[cfg_attr(feature = "schemars", derive(JsonSchema))]
14#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
15pub enum ComponentSourceRef {
16    /// Public OCI reference (`oci://...`).
17    Oci(String),
18    /// Private repository reference (`repo://...`).
19    Repo(String),
20    /// Store-licensed component reference (`store://...`).
21    Store(String),
22    /// File-based component reference (`file://...`).
23    File(String),
24}
25
26impl ComponentSourceRef {
27    /// Returns the scheme name for this reference.
28    pub fn scheme(&self) -> &'static str {
29        match self {
30            ComponentSourceRef::Oci(_) => "oci",
31            ComponentSourceRef::Repo(_) => "repo",
32            ComponentSourceRef::Store(_) => "store",
33            ComponentSourceRef::File(_) => "file",
34        }
35    }
36
37    /// Returns the raw reference portion without the scheme prefix.
38    pub fn reference(&self) -> &str {
39        match self {
40            ComponentSourceRef::Oci(value) => value,
41            ComponentSourceRef::Repo(value) => value,
42            ComponentSourceRef::Store(value) => value,
43            ComponentSourceRef::File(value) => value,
44        }
45    }
46}
47
48impl core::fmt::Display for ComponentSourceRef {
49    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
50        write!(f, "{}://{}", self.scheme(), self.reference())
51    }
52}
53
54impl core::str::FromStr for ComponentSourceRef {
55    type Err = ComponentSourceRefError;
56
57    fn from_str(value: &str) -> Result<Self, Self::Err> {
58        if value.is_empty() {
59            return Err(ComponentSourceRefError::EmptyReference);
60        }
61        if value.chars().any(char::is_whitespace) {
62            return Err(ComponentSourceRefError::ContainsWhitespace);
63        }
64        if value.starts_with("oci://") {
65            return parse_with_scheme(value, "oci://").map(ComponentSourceRef::Oci);
66        }
67        if value.starts_with("repo://") {
68            return parse_with_scheme(value, "repo://").map(ComponentSourceRef::Repo);
69        }
70        if value.starts_with("store://") {
71            return parse_with_scheme(value, "store://").map(ComponentSourceRef::Store);
72        }
73        if value.starts_with("file://") {
74            return parse_with_scheme(value, "file://").map(ComponentSourceRef::File);
75        }
76        Err(ComponentSourceRefError::InvalidScheme)
77    }
78}
79
80impl From<ComponentSourceRef> for String {
81    fn from(value: ComponentSourceRef) -> Self {
82        value.to_string()
83    }
84}
85
86impl TryFrom<String> for ComponentSourceRef {
87    type Error = ComponentSourceRefError;
88
89    fn try_from(value: String) -> Result<Self, Self::Error> {
90        value.parse()
91    }
92}
93
94/// Errors produced when parsing component source references.
95#[derive(Debug, thiserror::Error, PartialEq, Eq)]
96pub enum ComponentSourceRefError {
97    /// Reference cannot be empty.
98    #[error("component source reference cannot be empty")]
99    EmptyReference,
100    /// Reference must not contain whitespace.
101    #[error("component source reference must not contain whitespace")]
102    ContainsWhitespace,
103    /// Reference must use a supported scheme.
104    #[error("component source reference must use oci://, repo://, store://, or file://")]
105    InvalidScheme,
106    /// Reference is missing the required locator after the scheme.
107    #[error("component source reference is missing a locator")]
108    MissingLocator,
109}
110
111fn parse_with_scheme(value: &str, scheme: &str) -> Result<String, ComponentSourceRefError> {
112    if let Some(rest) = value.strip_prefix(scheme) {
113        if rest.is_empty() {
114            return Err(ComponentSourceRefError::MissingLocator);
115        }
116        return Ok(rest.to_string());
117    }
118    Err(ComponentSourceRefError::InvalidScheme)
119}